fictioneer/includes/functions/_service-caching.php
2024-06-17 22:12:39 +02:00

1075 lines
31 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

<?php
// =============================================================================
// ANY CACHE ACTIVE?
// =============================================================================
// List of known cache plugins
if ( ! defined( 'FICTIONEER_KNOWN_CACHE_PLUGINS' ) ) {
define(
'FICTIONEER_KNOWN_CACHE_PLUGINS',
array(
'w3-total-cache/w3-total-cache.php', // W3 Total Cache
'wp-super-cache/wp-cache.php', // WP Super Cache
'wp-rocket/wp-rocket.php', // WP Rocket
'litespeed-cache/litespeed-cache.php', // LiteSpeed Cache
'wp-fastest-cache/wpFastestCache.php', // WP Fastest Cache
'cache-enabler/cache-enabler.php', // Cache Enabler
'hummingbird-performance/wp-hummingbird.php', // Hummingbird Optimize Speed, Enable Cache
'wp-optimize/wp-optimize.php', // WP-Optimize - Clean, Compress, Cache
'sg-cachepress/sg-cachepress.php', // SG Optimizer (SiteGround)
'breeze/breeze.php', // Breeze (by Cloudways)
'nitropack/nitropack.php' // NitroPack
)
);
}
if ( ! function_exists( 'fictioneer_caching_active' ) ) {
/**
* Checks whether caching is active
*
* Checks for a number of known caching plugins or the cache
* compatibility option for anything not covered.
*
* @since 4.0.0
*
* @param string|null $context Context of the check. Currently unused.
*
* @return boolean Either true (active) or false (inactive or not installed).
*/
function fictioneer_caching_active( $context = null ) {
// Check early
if ( get_option( 'fictioneer_enable_cache_compatibility' ) ) {
return true;
}
// Check active plugins
$active_plugins = apply_filters( 'active_plugins', get_option( 'active_plugins' ) );
$active_cache_plugins = array_intersect( $active_plugins, FICTIONEER_KNOWN_CACHE_PLUGINS );
// Any cache plugins active?
return ! empty( $active_cache_plugins );
}
}
if ( ! function_exists( 'fictioneer_private_caching_active' ) ) {
/**
* Checks whether private caching is active
*
* Checks whether the user is logged in and the private cache
* compatibility mode is enabled. Public and private caching
* contradict each other, with public caching given priority.
*
* @since 5.0.0
*
* @return boolean Either true or false.
*/
function fictioneer_private_caching_active() {
return is_user_logged_in() &&
get_option( 'fictioneer_enable_private_cache_compatibility', false ) &&
! get_option( 'fictioneer_enable_public_cache_compatibility', false );
}
}
// Boolean: Partial caching enabled?
if ( ! defined( 'FICTIONEER_ENABLE_PARTIAL_CACHING' ) ) {
define(
'FICTIONEER_ENABLE_PARTIAL_CACHING',
get_option( 'fictioneer_enable_static_partials' ) &&
! is_customize_preview() &&
! fictioneer_caching_active( 'enable_partial_caching' )
);
}
// =============================================================================
// ENABLE SHORTCODE TRANSIENTS?
// =============================================================================
/**
* Return whether shortcode Transients should be enabled
*
* @since 5.6.3
*
* @return boolean Either true or false.
*/
function fictioneer_enable_shortcode_transients() {
// Disable in Customizer
if ( is_customize_preview() ) {
return false;
}
// Check constant and caching status
$bool = FICTIONEER_SHORTCODE_TRANSIENT_EXPIRATION > -1 && ! fictioneer_caching_active( 'shortcodes' );
// Filter
$bool = apply_filters( 'fictioneer_filter_enable_shortcode_transients', $bool );
// Return
return $bool;
}
// =============================================================================
// PURGE CACHES
// =============================================================================
if ( ! function_exists( 'fictioneer_purge_all_caches' ) ) {
/**
* Trigger the "purge all" functions of known cache plugins
*
* @since 4.0.0
* @link https://docs.litespeedtech.com/lscache/lscwp/api/#purge
* @link https://docs.wp-rocket.me/article/494-how-to-clear-cache-via-cron-job#clear-cache
* @link https://wp-kama.com/plugin/wp-super-cache/function/wp_cache_clean_cache
*/
function fictioneer_purge_all_caches() {
// WordPress Query Cache
wp_cache_flush();
// Hook for additional purges
do_action( 'fictioneer_cache_purge_all' );
// LiteSpeed Cache
if ( class_exists( '\LiteSpeed\Purge' ) ) {
do_action( 'litespeed_purge_all' );
}
// WP Rocket Cache
if ( function_exists( 'rocket_clean_domain' ) ) {
rocket_clean_domain();
}
// WP Super Cache
if ( function_exists( 'wp_cache_clean_cache' ) ) {
global $file_prefix;
wp_cache_clean_cache( $file_prefix, true );
}
// W3 Total Cache
if ( function_exists( 'w3tc_flush_all' ) ) {
w3tc_flush_all();
}
// Cache Enabler
if ( has_action( 'cache_enabler_clear_complete_cache' ) ) {
do_action( 'cache_enabler_clear_complete_cache' );
}
// Hummingbird
if ( has_action( 'wphb_clear_page_cache' ) ) {
do_action( 'wphb_clear_page_cache' );
}
// SG Optimizer
// Insufficient or hard-to-find documentation and if the
// developer cannot be bothered, neither can I.
// Breeze
if ( has_action( 'breeze_clear_all_cache' ) ) {
do_action( 'breeze_clear_all_cache' );
}
// WP Optimize
if ( class_exists( 'WP_Optimize' ) ) {
$wp_optimize = WP_Optimize();
if ( method_exists( $wp_optimize, 'get_page_cache' ) ) {
$wp_optimize->get_page_cache()->purge();
}
}
// W3 Fastest Cache
if ( function_exists( 'wpfc_clear_all_cache' ) ) {
wpfc_clear_all_cache();
}
// NitroPack
// Insufficient or hard-to-find documentation and if the
// developer cannot be bothered, neither can I.
// Cached HTML partials
fictioneer_clear_all_cached_partials();
}
}
if ( ! function_exists( 'fictioneer_purge_post_cache' ) ) {
/**
* Trigger the "purge post" functions of known cache plugins
*
* @since 4.7.0
* @link https://docs.litespeedtech.com/lscache/lscwp/api/#purge
* @link https://docs.wp-rocket.me/article/494-how-to-clear-cache-via-cron-job#clear-cache
* @link https://wp-kama.com/plugin/wp-super-cache/function/wpsc_delete_post_cache
*
* @param int $post_id Post ID.
*/
function fictioneer_purge_post_cache( $post_id ) {
// Setup
$post_id = fictioneer_validate_id( $post_id );
// Abort if...
if ( empty( $post_id ) ) {
return;
}
// Hook for additional purges
do_action( 'fictioneer_cache_purge_post', $post_id );
// WordPress Query Cache
clean_post_cache( $post_id );
// LiteSpeed Cache
if ( class_exists( '\LiteSpeed\Purge' ) ) {
do_action( 'litespeed_purge_post', $post_id );
}
// WP Rocket Cache
if ( function_exists( 'rocket_clean_post' ) ) {
rocket_clean_post( $post_id );
}
// WP Super Cache
if ( function_exists( 'wp_cache_post_change' ) ) {
wp_cache_post_change( $post_id );
}
// W3 Total Cache
if ( function_exists( 'w3tc_flush_post' ) ) {
w3tc_flush_post( $post_id );
}
// Cache Enabler
if ( has_action( 'cache_enabler_clear_page_cache_by_post' ) ) {
do_action( 'cache_enabler_clear_page_cache_by_post', $post_id );
}
// Hummingbird
if ( has_action( 'wphb_clear_page_cache' ) ) {
do_action( 'wphb_clear_page_cache', $post_id );
}
// SG Optimizer
// Insufficient or hard-to-find documentation and if the
// developer cannot be bothered, neither can I.
// Breeze
// Insufficient or hard-to-find documentation and if the
// developer cannot be bothered, neither can I.
// WP Optimize
if (
class_exists( 'WPO_Page_Cache' ) &&
method_exists( 'WPO_Page_Cache', 'delete_single_post_cache' )
) {
WPO_Page_Cache::delete_single_post_cache( $post_id );
}
// W3 Fastest Cache
if ( function_exists( 'wpfc_clear_post_cache_by_id' ) ) {
wpfc_clear_post_cache_by_id( $post_id );
}
// NitroPack
// Insufficient or hard-to-find documentation and if the
// developer cannot be bothered, neither can I.
// Cached HTML partial
if ( FICTIONEER_ENABLE_PARTIAL_CACHING ) {
fictioneer_clear_cached_content( $post_id );
}
}
}
// =============================================================================
// LITESPEED CACHE
// =============================================================================
if ( ! function_exists( 'fictioneer_litespeed_running' ) ) {
/**
* Checks whether LiteSpeed Cache plugin is active
*
* @since 4.0.0
*
* @return boolean Either true (active) or false (inactive or not installed)
*/
function fictioneer_litespeed_running() {
return in_array(
'litespeed-cache/litespeed-cache.php',
apply_filters( 'active_plugins', get_option('active_plugins' ) )
);
}
}
// =============================================================================
// PURGE RELEVANT CACHE(S) ON POST SAVE
// =============================================================================
if ( ! function_exists( 'fictioneer_purge_template_caches' ) ) {
/**
* Purges all posts with the given template template
*
* @since 5.5.2
*
* @param string $template The page template (without `.php`)
*/
function fictioneer_purge_template_caches( $template ) {
// Setup (this should normally only yield one page or none)
$pages = get_posts(
array(
'post_type' => 'page',
'numberposts' => -1,
'fields' => 'ids',
'meta_key' => '_wp_page_template',
'meta_value' => "{$template}.php",
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false, // Improve performance
'no_found_rows' => true // Improve performance
)
);
// Purge
if ( $pages ) {
foreach ( $pages as $page_id ) {
fictioneer_purge_post_cache( $page_id );
}
}
}
}
if ( ! function_exists( 'fictioneer_refresh_post_caches' ) ) {
/**
* Purges relevant caches when a post is updated
*
* "There are only two hard things in Computer Science: cache invalidation and
* naming things" -- Phil Karlton.
*
* @since 5.0.0
*
* @param int $post_id Updated post ID.
*/
function fictioneer_refresh_post_caches( $post_id ) {
// Prevent multi-fire
if ( fictioneer_multi_save_guard( $post_id ) ) {
return;
}
// Purge all?
if ( get_option( 'fictioneer_purge_all_caches' ) ) {
fictioneer_purge_all_caches();
return;
}
// Get type
$post_type = get_post_type( $post_id );
if ( ! $post_type ) {
return;
}
// Start system action: unset user
$current_user_id = get_current_user_id();
wp_set_current_user( 0 );
// Remove actions to prevent infinite loops and multi-fire
fictioneer_toggle_refresh_hooks( false );
fictioneer_toggle_transient_purge_hooks( false );
fictioneer_toggle_update_tracker_hooks( false );
// Purge updated post
fictioneer_purge_post_cache( $post_id );
// Purge front page (if any)
$font_page_id = intval( get_option( 'page_on_front' ) ?: -1 );
if ( $font_page_id != $post_id && $font_page_id > 0 ) {
fictioneer_purge_post_cache( $font_page_id );
}
// Purge associated list pages
if ( in_array( $post_type, ['fcn_chapter', 'fcn_story'] ) ) {
fictioneer_purge_template_caches( 'chapters' );
fictioneer_purge_template_caches( 'stories' );
}
if ( $post_type == 'fcn_recommendation' ) {
fictioneer_purge_template_caches( 'recommendations' );
}
// Purge parent story (if any)...
if ( $post_type == 'fcn_chapter' ) {
$story_id = get_post_meta( $post_id, 'fictioneer_chapter_story', true );
if ( ! empty( $story_id ) ) {
$chapters = fictioneer_get_story_chapter_ids( $story_id );
delete_post_meta( $story_id, 'fictioneer_story_data_collection' );
delete_post_meta( $story_id, 'fictioneer_story_chapter_index_html' );
fictioneer_purge_post_cache( $story_id );
// ... and associated chapters
if ( ! empty( $chapters ) ) {
foreach ( $chapters as $chapter_id ) {
fictioneer_purge_post_cache( $chapter_id );
}
}
}
}
// Purge associated chapters
if ( $post_type == 'fcn_story' ) {
$chapters = fictioneer_get_story_chapter_ids( $post_id );
if ( ! empty( $chapters ) ) {
foreach ( $chapters as $chapter_id ) {
fictioneer_purge_post_cache( $chapter_id );
}
}
}
// Purge all collections (cheapest option)
if ( $post_type != 'page' ) {
fictioneer_purge_template_caches( 'collections' );
$collections = get_posts(
array(
'post_type' => 'fcn_collection',
'numberposts' => -1,
'fields' => 'ids',
'update_post_meta_cache' => false, // Improve performance
'update_post_term_cache' => false, // Improve performance
'no_found_rows' => true // Improve performance
)
);
foreach ( $collections as $collection_id ) {
fictioneer_purge_post_cache( $collection_id );
}
}
// Purge relationships
if ( FICTIONEER_RELATIONSHIP_PURGE_ASSIST ) {
$registry = fictioneer_get_relationship_registry();
// Always purge...
foreach ( $registry['always'] as $key => $entry ) {
delete_post_meta( $key, 'fictioneer_schema' );
fictioneer_purge_post_cache( $key );
}
// Direct relationships
if ( isset( $registry[ $post_id ] ) ) {
foreach ( $registry[ $post_id ] as $key => $entry ) {
delete_post_meta( $key, 'fictioneer_schema' );
fictioneer_purge_post_cache( $key );
}
}
// Purge post relationships
$relation = false;
switch ( $post_type ) {
case 'post':
$relation = 'ref_posts';
break;
case 'fcn_chapter':
$relation = 'ref_chapters';
break;
case 'fcn_story':
$relation = 'ref_stories';
break;
case 'fcn_recommendation':
$relation = 'ref_recommendations';
break;
}
if ( $relation ) {
foreach ( $registry[ $relation ] as $key => $entry ) {
delete_post_meta( $key, 'fictioneer_schema' );
fictioneer_purge_post_cache( $key );
}
}
}
// Restore actions
fictioneer_toggle_refresh_hooks();
fictioneer_toggle_transient_purge_hooks();
fictioneer_toggle_update_tracker_hooks();
// End system action: restore user
wp_set_current_user( $current_user_id );
}
}
/**
* Add or remove actions for `fictioneer_refresh_post_caches`
*
* @since 5.5.2
*
* @param boolean $add Optional. Whether to add or remove the action. Default true.
*/
function fictioneer_toggle_refresh_hooks( $add = true ) {
$hooks = ['save_post', 'untrash_post', 'trashed_post', 'delete_post'];
if ( $add ) {
foreach ( $hooks as $hook ) {
add_action( $hook, 'fictioneer_refresh_post_caches', 20 );
}
} else {
foreach ( $hooks as $hook ) {
remove_action( $hook, 'fictioneer_refresh_post_caches', 20 );
}
}
}
if ( FICTIONEER_CACHE_PURGE_ASSIST && fictioneer_caching_active( 'purge_assist' ) ) {
fictioneer_toggle_refresh_hooks();
}
// =============================================================================
// RELATIONSHIP REGISTRY
//
// The relationship directory array is not fail-safe. Technically, it can happen
// that while the registry is pulled for updating by one post, it is also pulled
// by another, causing the last to override the first without the first's data.
// However, this is unlikely and not the end of the world.
//
// A possible solution would be to create a new database table for registry
// updates which is processed by a worker occasionally. Cannot be bothered tho.
// =============================================================================
/**
* Returns relationship registry from options table
*
* @since 5.0.0
*
* @return array The balanced array.
*/
function fictioneer_get_relationship_registry() {
// Setup
$registry = get_option( 'fictioneer_relationship_registry', [] );
$registry = is_array( $registry ) ? $registry : [];
// Important nodes
$nodes = ['ref_posts', 'ref_chapters', 'ref_stories', 'ref_recommendations', 'always'];
foreach ( $nodes as $node ) {
if ( ! isset( $registry[ $node ] ) ) {
$registry[ $node ] = [];
}
if ( ! is_array( $registry[ $node ] ) ) {
$registry[ $node ] = [];
}
}
// Return
return $registry;
}
/**
* Saves relationship registry to options table
*
* @since 5.0.0
*
* @param array $registry Current registry to override the stored option.
*
* @return array True if the value was updated, false otherwise.
*/
function fictioneer_save_relationship_registry( $registry ) {
return update_option( 'fictioneer_relationship_registry', $registry );
}
/**
* Remove relationships on delete
*
* @since 5.0.0
*
* @param int $post_id The deleted post ID.
*/
function fictioneer_delete_relationship( $post_id ) {
// Setup
$registry = fictioneer_get_relationship_registry();
// Remove node (if set)
unset( $registry[ $post_id ] );
// Remove references (if any)
foreach ( $registry as $key => $entry ) {
unset( $registry[ $key ][ $post_id ] );
}
unset( $registry['ref_posts'][ $post_id ] );
unset( $registry['ref_chapters'][ $post_id ] );
unset( $registry['ref_stories'][ $post_id ] );
unset( $registry['ref_recommendations'][ $post_id ] );
unset( $registry['always'][ $post_id ] );
// Update database
fictioneer_save_relationship_registry( $registry );
}
if ( FICTIONEER_RELATIONSHIP_PURGE_ASSIST ) {
add_action( 'delete_post', 'fictioneer_delete_relationship', 100 );
}
// =============================================================================
// TRACK CHAPTER & STORY UPDATES
// =============================================================================
if ( ! function_exists( 'fictioneer_track_chapter_and_story_updates' ) ) {
/**
* Updates Transients and storage options when a story or chapter is updated
*
* Whenever a story or chapter is saved, trashed, restored, or permanently
* deleted, this function will update or purge respective Transients and
* storage options to reflect the current state of content. This is mainly
* used for caching purposes and there is no harm if these values vanish.
*
* @since 4.7.0
*
* @param int $post_id Updated post ID.
*/
function fictioneer_track_chapter_and_story_updates( $post_id ) {
// Prevent multi-fire
if ( fictioneer_multi_save_guard( $post_id ) ) {
return;
}
// Get story ID from post or parent story (if any)
$post_type = get_post_type( $post_id ); // Not all hooks get the $post object!
$story_id = $post_type == 'fcn_story' ? $post_id : get_post_meta( $post_id, 'fictioneer_chapter_story', true );
// Delete cached statistics for stories
delete_transient( 'fictioneer_stories_statistics' );
delete_transient( 'fictioneer_stories_total_word_count' );
// If there is a story...
if ( ! empty( $story_id ) ) {
// Decides when cached story/chapter data need to be refreshed (only used for Follows)
// Beware: This is an option, not a Transient!
update_option( 'fictioneer_story_or_chapter_updated_timestamp', time() * 1000 );
// Refresh cached HTML output
delete_transient( 'fictioneer_story_chapter_list_html' . $story_id );
// Delete cached API response
if ( FICTIONEER_API_STORYGRAPH_TRANSIENTS ) {
$count_stories = wp_count_posts( 'fcn_story' );
$pages = $count_stories->publish / FICTIONEER_API_STORYGRAPH_STORIES_PER_PAGE + 1;
delete_transient( 'fictioneer_api_story_' . $story_id );
for ( $i = 1; $i <= $pages; $i++ ) {
delete_transient( 'fictioneer_storygraph_stories_' . $i );
}
}
}
}
}
/**
* Add or remove actions for `fictioneer_toggle_update_tracker_hooks`
*
* @since 5.5.2
*
* @param boolean $add Optional. Whether to add or remove the action. Default true.
*/
function fictioneer_toggle_update_tracker_hooks( $add = true ) {
$hooks = ['save_post', 'untrash_post', 'trashed_post', 'delete_post'];
if ( $add ) {
foreach ( $hooks as $hook ) {
add_action( $hook, 'fictioneer_track_chapter_and_story_updates', 10 );
}
} else {
foreach ( $hooks as $hook ) {
remove_action( $hook, 'fictioneer_track_chapter_and_story_updates', 10 );
}
}
}
fictioneer_toggle_update_tracker_hooks();
// =============================================================================
// PURGE CACHE TRANSIENTS
// =============================================================================
/**
* Purge Transients used for caching when posts are updated
*
* @since 5.4.9
*
* @param int $post_id Updated post ID.
*/
function fictioneer_purge_transients( $post_id ) {
// Prevent multi-fire
if ( fictioneer_multi_save_guard( $post_id ) ) {
return;
}
// Setup
$post_type = get_post_type( $post_id ); // Not all hooks get the $post object!
// Menus
fictioneer_purge_nav_menu_transients();
// Shortcode...
if ( fictioneer_enable_shortcode_transients() ) {
// Recommendation?
if ( $post_type == 'fcn_recommendation' ) {
fictioneer_delete_transients_like( 'fictioneer_shortcode_latest_recommendations' );
return;
}
// All
fictioneer_delete_transients_like( 'fictioneer_shortcode' );
}
}
/**
* Add or remove actions for `fictioneer_purge_transients`
*
* @since 5.5.2
*
* @param boolean $add Optional. Whether to add or remove the action. Default true.
*/
function fictioneer_toggle_transient_purge_hooks( $add = true ) {
$hooks = ['save_post', 'untrash_post', 'trashed_post', 'delete_post'];
if ( $add ) {
foreach ( $hooks as $hook ) {
add_action( $hook, 'fictioneer_purge_transients' );
}
} else {
foreach ( $hooks as $hook ) {
remove_action( $hook, 'fictioneer_purge_transients' );
}
}
}
fictioneer_toggle_transient_purge_hooks();
/**
* Purge nav menu Transients on menu updates
*
* @since 5.6.0
*/
function fictioneer_purge_nav_menu_transients() {
delete_transient( 'fictioneer_main_nav_menu_html' );
delete_transient( 'fictioneer_footer_menu_html' );
}
add_action( 'wp_update_nav_menu', 'fictioneer_purge_nav_menu_transients' );
// =============================================================================
// PARTIAL CACHING (THEME)
// =============================================================================
/**
* Generate random salt for cache-related hashes
*
* Note: Saves the salt to the option 'fictioneer_cache_salt'.
*
* @since 5.18.3
*
* @return string The generated salt.
*/
function fictioneer_generate_cache_salt() {
$salt = bin2hex( random_bytes( 32 / 2 ) );
update_option( 'fictioneer_cache_salt', $salt );
return $salt;
}
/**
* Get salt for cache-related hashes
*
* Note: Regenerated if no salt exists.
*
* @since 5.18.3
*
* @return string The salt.
*/
function fictioneer_get_cache_salt() {
return get_option( 'fictioneer_cache_salt' ) ?: fictioneer_generate_cache_salt();
}
/**
* Get static HTML of cached template partial
*
* @since 5.18.3
* @see get_template_directory()
*
* @param string|null $dir Optional. Directory path to override the default.
*
* @return bool True if the directory exists or has been created, false on failure.
*/
function fictioneer_create_html_cache_directory( $dir = null ) {
// Setup
$default_dir = get_template_directory() . '/cache/html/';
$dir = $dir ?? $default_dir;
$result = true;
// Create cache directories if missing
if ( ! is_dir( $dir ) ) {
$result = mkdir( $dir, 0755, true );
}
// Hide files from index
if ( $result && ! file_exists( $dir . '/index.php' ) ) {
file_put_contents( $dir . '/index.php', "<?php\n// Silence is golden.\n" );
}
if ( $result && ! file_exists( $default_dir . '/index.php' ) ) {
file_put_contents( $default_dir . '/index.php', "<?php\n// Silence is golden.\n" );
}
// Add .htaccess to limit access
if ( $result && ! file_exists( $dir . '/.htaccess' ) ) {
file_put_contents(
$dir . '/.htaccess',
"<Files *>\n Order Allow,Deny\n Deny from all\n</Files>\n"
);
}
if ( $result && ! file_exists( $default_dir . '/.htaccess' ) ) {
file_put_contents(
$default_dir . '/.htaccess',
"<Files *>\n Order Allow,Deny\n Deny from all\n</Files>\n"
);
}
// Success or failure
return $result;
}
/**
* Get static HTML of cached template partial
*
* @since 5.18.1
* @see get_template_part()
*
* @param string $slug The slug name for the generic template.
* @param string $identifier Optional. Additional identifier string for the file. Default empty string.
* @param int|null $expiration Optional. Seconds until the cache expires. Default 24 hours in seconds.
* @param string|null $name Optional. The name of the specialized template.
* @param array $args Optional. Additional arguments passed to the template.
*/
function fictioneer_get_cached_partial( $slug, $identifier = '', $expiration = null, $name = null, $args = [] ) {
// Use default function if...
if ( ! FICTIONEER_ENABLE_PARTIAL_CACHING ) {
get_template_part( $slug, $name, $args );
return;
}
// Setup
$args_hash = md5( serialize( $args ) . $identifier . fictioneer_get_cache_salt() );
$static_file = $slug . ( $name ? "-{$name}" : '' ) . "-{$args_hash}.html";
$path = get_template_directory() . '/cache/html/' . $static_file;
// Make sure directory exists and handle failure
if ( ! fictioneer_create_html_cache_directory( dirname( $path ) ) ) {
get_template_part( $slug, $name, $args );
return;
}
// Get static file if not expired
if ( file_exists( $path ) ) {
$filemtime = filemtime( $path );
if ( time() - $filemtime < ( $expiration ?? FICTIONEER_PARTIAL_CACHE_EXPIRATION_TIME ) ) {
echo file_get_contents( $path );
return;
}
}
// Generate and cache the new file
ob_start();
get_template_part( $slug, $name, $args );
$html = ob_get_clean();
if ( file_put_contents( $path, $html ) === false ) {
get_template_part( $slug, $name, $args );
} else {
echo $html;
}
}
/**
* Clears static HTML of all cached partials
*
* Note: Only executed once per request to reduce overhead.
* Can therefore be used with impunity.
*
* @since 5.18.1
*/
function fictioneer_clear_all_cached_partials() {
// Only run this once per request
static $done = false;
if ( $done || ! get_option( 'fictioneer_enable_static_partials' ) ) {
return;
}
$done = true;
// Setup
$cache_dir = get_template_directory() . '/cache/html/';
// Regenerate cache salt
fictioneer_generate_cache_salt();
// Ensure the directory exists
if ( ! file_exists( $cache_dir ) ) {
return;
}
// Recursively delete files and subdirectories
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator( $cache_dir, RecursiveDirectoryIterator::SKIP_DOTS ),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ( $files as $fileinfo ) {
$todo = ( $fileinfo->isDir() ? 'rmdir' : 'unlink' );
if ( ! $todo( $fileinfo->getRealPath() ) ) {
error_log( 'Failed to delete ' . $fileinfo->getRealPath() );
}
}
}
/**
* Returns post content from cached HTML file
*
* @since 5.19.0
* @link https://developer.wordpress.org/reference/functions/the_content/
* @global WP_Post $post
*
* @param string|null $more_link_text Optional. Content for when there is more text.
* @param bool $strip_teaser Optional. Strip teaser content before the more text.
*
* @return string The post content.
*/
function fictioneer_get_static_content( $more_link_text = \null, $strip_teaser = \false ) {
global $post;
// Use default function if...
if (
! FICTIONEER_ENABLE_PARTIAL_CACHING ||
! empty( $post->post_password ) ||
$post->post_status !== 'publish' ||
get_post_meta( $post->ID, 'fictioneer_chapter_disable_partial_caching', true ) ||
! apply_filters( 'fictioneer_filter_static_content_true', true, $post )
) {
$content = get_the_content( $more_link_text, $strip_teaser );
$content = apply_filters( 'the_content', $content );
$content = str_replace( ']]>', ']]&gt;', $content );
return apply_filters( 'fictioneer_filter_static_content', $content, $post );
}
// Setup
$hash = md5( $post->ID . fictioneer_get_cache_salt() );
$dir = get_template_directory() . '/cache/html/' . substr( $hash, 0, 2 );
$path = "{$dir}/{$hash}_{$post->ID}.html";
// Make sure directory exists and handle failure
if ( ! fictioneer_create_html_cache_directory( $dir ) ) {
$content = get_the_content( $more_link_text, $strip_teaser );
$content = apply_filters( 'the_content', $content );
$content = str_replace( ']]>', ']]&gt;', $content );
return apply_filters( 'fictioneer_filter_static_content', $content, $post );
}
// Get static file if not expired
if ( file_exists( $path ) ) {
$filemtime = filemtime( $path );
if ( time() - $filemtime < FICTIONEER_PARTIAL_CACHE_EXPIRATION_TIME ) {
return apply_filters( 'fictioneer_filter_static_content', file_get_contents( $path ), $post );
}
}
// Get content and apply filters
$content = get_the_content( $more_link_text, $strip_teaser );
$content = apply_filters( 'the_content', $content );
$content = str_replace( ']]>', ']]&gt;', $content );
// Cache as static file
file_put_contents( $path, $content );
// Return content
return apply_filters( 'fictioneer_filter_static_content', $content, $post );
}
/**
* Renders post content from cached HTML file
*
* @since 5.19.0
*
* @param string|null $more_link_text Optional. Content for when there is more text.
* @param bool $strip_teaser Optional. Strip teaser content before the more text.
*/
function fictioneer_the_static_content( $more_link_text = \null, $strip_teaser = \false ) {
echo fictioneer_get_static_content( $more_link_text, $strip_teaser );
}
/**
* Clears static content HTML of post
*
* @since 5.19.0
*
* @param int $post_id ID of the post to clear the cache for.
*
* @return bool True if the cache file has been deleted, false if not found.
*/
function fictioneer_clear_cached_content( $post_id ) {
// Setup
$hash = md5( $post_id . fictioneer_get_cache_salt() );
$dir = get_template_directory() . '/cache/html/' . substr( $hash, 0, 2 );
$path = "{$dir}/{$hash}_{$post_id}.html";
// Delete file
if ( file_exists( $path ) ) {
unlink( $path );
return true;
}
// File not found
return false;
}
if ( FICTIONEER_ENABLE_PARTIAL_CACHING ) {
foreach ( ['save_post', 'untrash_post', 'trashed_post', 'delete_post'] as $hook ) {
add_action( $hook, 'fictioneer_clear_cached_content' );
}
}