fictioneer/includes/functions/_service-caching.php
2024-12-06 17:38:32 +01:00

1477 lines
41 KiB
PHP
Raw Permalink 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
)
);
}
function fictioneer_get_active_cache_plugins() {
static $plugin_names = 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',
'wp-optimize/wp-optimize.php' => 'WP-Optimize',
'sg-cachepress/sg-cachepress.php' => 'SG Optimizer',
'breeze/breeze.php' => 'Breeze',
'nitropack/nitropack.php' => 'NitroPack'
);
$result = [];
foreach ( FICTIONEER_KNOWN_CACHE_PLUGINS as $plugin_slug ) {
if ( is_plugin_active( $plugin_slug ) && isset( $plugin_names[ $plugin_slug ] ) ) {
$result[] = $plugin_names[ $plugin_slug ];
}
}
return $result;
}
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' )
);
}
// Boolean: Story card caching enabled?
if ( ! defined( 'FICTIONEER_ENABLE_STORY_CARD_CACHING' ) ) {
define(
'FICTIONEER_ENABLE_STORY_CARD_CACHING',
get_option( 'fictioneer_enable_story_card_caching' ) && ! is_customize_preview()
);
}
// =============================================================================
// ENABLE TRANSIENTS?
// =============================================================================
/**
* Whether to enable Transients for shortcodes
*
* @since 5.6.3
* @since 5.23.1 - Do not turn off with cache plugin.
* @since 5.25.0 - Refactored with option.
*
* @param string $shortcode The shortcode in question.
*
* @return boolean Either true or false.
*/
function fictioneer_enable_shortcode_transients( $shortcode = null ) {
global $pagenow;
if ( is_customize_preview() || is_admin() || $pagenow === 'post.php' ) {
return false;
}
$bool = FICTIONEER_SHORTCODE_TRANSIENT_EXPIRATION > -1 &&
! get_option( 'fictioneer_disable_shortcode_transients' );
return apply_filters( 'fictioneer_filter_enable_shortcode_transients', $bool, $shortcode );
}
/**
* Whether to enable Transients for story chapter lists
*
* Note: By default true unless there is a known cache plugin
* active or the Transients are turned off by option. They are
* always disabled in the Customizer preview.
*
* @since 5.25.0
*
* @param int $post_id Post ID of the story.
*
* @return boolean Either true or false.
*/
function fictioneer_enable_chapter_list_transients( $post_id ) {
if ( is_customize_preview() ) {
return false;
}
$bool = ! fictioneer_caching_active( 'story_chapter_list' ) &&
! get_option( 'fictioneer_disable_chapter_list_transients' );
return apply_filters( 'fictioneer_filter_enable_chapter_list_transients', $bool, $post_id );
}
/**
* Whether to enable Transients for menus
*
* @since 5.25.0
*
* @param string $location Location identifier of the menu. Possible locations are
* 'nav_menu', 'mobile_nav_menu', and 'footer_menu'.
*
* @return boolean Either true or false.
*/
function fictioneer_enable_menu_transients( $location ) {
if ( is_customize_preview() ) {
return false;
}
return apply_filters(
'fictioneer_filter_enable_menu_transients',
! get_option( 'fictioneer_disable_menu_transients' ),
$location
);
}
// =============================================================================
// 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() {
// Object 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();
// Cached story cards
if ( FICTIONEER_ENABLE_STORY_CARD_CACHING ) {
fictioneer_purge_story_card_cache();
}
}
}
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 );
}
// Cached story card
if ( FICTIONEER_ENABLE_STORY_CARD_CACHING ) {
fictioneer_delete_cached_story_card( $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 = fictioneer_get_chapter_story_id( $post_id );
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 : fictioneer_get_chapter_story_id( $post_id );
// 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 );
// Clear meta caches
delete_post_meta( $story_id, 'fictioneer_story_data_collection' );
delete_post_meta( $story_id, 'fictioneer_story_chapter_index_html' );
// Delete cached query results
fictioneer_delete_transients_like( "fictioneer_query_{$story_id}" );
// Refresh cached HTML output
delete_transient( 'fictioneer_story_chapter_list_html_' . $story_id );
}
}
}
/**
* 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' );
}
} else {
foreach ( $hooks as $hook ) {
remove_action( $hook, 'fictioneer_track_chapter_and_story_updates' );
}
}
}
fictioneer_toggle_update_tracker_hooks();
/**
* Rebuild story data collection after purge
*
* Note: This is only triggered for one story to avoid rebuilding the
* collections of ALL stories at once in case of a complete purge.
*
* @since 5.23.1
*
* @param int $post_id Updated post ID.
* @param WP_Post $post The post object.
*/
function fictioneer_rebuild_story_data_collection( $post_id, $post ) {
// Prevent multi-fire and check type
if (
fictioneer_multi_save_guard( $post_id ) ||
! in_array( $post->post_type, ['fcn_story', 'fcn_chapter'] )
) {
return;
}
// Story or chapter?
if ( $post->post_type === 'fcn_chapter' ) {
$story_id = fictioneer_get_chapter_story_id( $post_id );
if ( $story_id ) {
fictioneer_get_story_data( $story_id );
}
} else {
fictioneer_get_story_data( $post_id );
}
// Prevent chain reaction
remove_action( 'save_post', 'fictioneer_rebuild_story_data_collection', 999 );
}
if ( FICTIONEER_ENABLE_STORY_DATA_META_CACHE ) {
add_action( 'save_post', 'fictioneer_rebuild_story_data_collection', 999, 2 );
}
// =============================================================================
// PURGE CACHE TRANSIENTS
// =============================================================================
/**
* Purge layout-related Transients
*
* @since 5.25.0
*/
function fictioneer_delete_layout_transients() {
fictioneer_purge_nav_menu_transients();
fictioneer_purge_story_card_cache();
fictioneer_delete_transients_like( 'fictioneer_shortcode' );
fictioneer_delete_transients_like( 'fictioneer_taxonomy_submenu_' );
delete_transient( 'fictioneer_dynamic_scripts_version' );
}
/**
* Purge Transients used for caching when posts are updated
*
* @since 5.4.9
* @since 5.23.0 - Refactored to purge all shortcode Transients.
*
* @param int $post_id Updated post ID.
*/
function fictioneer_purge_transients_after_update( $post_id ) {
// Prevent multi-fire
if ( fictioneer_multi_save_guard( $post_id ) ) {
return;
}
// Menus
fictioneer_purge_nav_menu_transients();
// Shortcodes...
fictioneer_delete_transients_like( 'fictioneer_shortcode' );
}
/**
* Add or remove actions for `fictioneer_purge_transients_after_update`
*
* @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_after_update' );
}
} else {
foreach ( $hooks as $hook ) {
remove_action( $hook, 'fictioneer_purge_transients_after_update' );
}
}
}
fictioneer_toggle_transient_purge_hooks();
/**
* Purge nav menu Transients
*
* @since 5.6.0
*/
function fictioneer_purge_nav_menu_transients() {
delete_transient( 'fictioneer_main_nav_menu_html' );
delete_transient( 'fictioneer_footer_menu_html' );
delete_transient( 'fictioneer_mobile_nav_menu_html' );
fictioneer_delete_transients_like( 'fictioneer_taxonomy_submenu_' );
}
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
*
* @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 = fictioneer_get_theme_cache_dir( 'create_html_cache_directory' ) . '/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
*
* @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 = fictioneer_get_theme_cache_dir( 'get_cached_partial' ) . '/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 = fictioneer_get_theme_cache_dir( 'clear_all_cached_partials' ) . '/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 = fictioneer_get_theme_cache_dir( 'get_static_content' ) . '/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 = fictioneer_get_theme_cache_dir( 'clear_cached_content' ) . '/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' );
}
}
// =============================================================================
// TRANSIENT CARD CACHE
// =============================================================================
/**
* Checks whether the story card cache is locked by another process
*
* Note: Locks the Transient for 30 seconds if currently unlocked,
* which needs to be cleared afterwards. The lock state for this
* process is cached in a static variable.
*
* @since 5.22.3
*
* @return bool True if locked, false if free for this process.
*/
function fictioneer_story_card_cache_lock() {
static $lock = null;
if ( $lock !== null ) {
return $lock;
}
if ( get_transient( 'fictioneer_story_card_cache_lock' ) ) {
$lock = true;
} else {
set_transient( 'fictioneer_story_card_cache_lock', 1, 30 );
$lock = false;
}
return $lock;
}
/**
* Returns the Transient array with all cached story cards
*
* @since 5.22.2
*
* @return array Array of cached story cards; empty array if disabled.
*/
function fictioneer_get_story_card_cache() {
// Abort if...
if ( ! FICTIONEER_ENABLE_STORY_CARD_CACHING ) {
return [];
}
// Initialize lock to avoid race conditions
fictioneer_story_card_cache_lock();
// Initialize global cache variable
global $fictioneer_story_card_cache;
if ( ! $fictioneer_story_card_cache ) {
$fictioneer_story_card_cache = get_transient( 'fictioneer_card_cache' ) ?: [];
}
return $fictioneer_story_card_cache;
}
/**
* Adds a story card to the cache
*
* @since 5.22.2
*
* @param string $key The cache key of the card.
* @param string $card The HTML of the card.
*/
function fictioneer_cache_story_card( $key, $card ) {
// Abort if...
if ( ! FICTIONEER_ENABLE_STORY_CARD_CACHING || fictioneer_story_card_cache_lock() ) {
return;
}
// Initialize global cache variable
global $fictioneer_story_card_cache;
// Setup
if ( ! $fictioneer_story_card_cache ) {
$fictioneer_story_card_cache = fictioneer_get_story_card_cache();
}
// Remove cached card (if set)...
unset( $fictioneer_story_card_cache[ $key ] );
// ... and append at the end
$fictioneer_story_card_cache[ $key ] = $card;
}
/**
* Returns the cached HTML for a specific story card
*
* @since 5.22.2
*
* @param string $key The cache key of the card.
*
* @return string|null HTML of the card or null if not found/disabled.
*/
function fictioneer_get_cached_story_card( $key ) {
// Abort if...
if ( ! FICTIONEER_ENABLE_STORY_CARD_CACHING ) {
return null;
}
// Initialize global cache variable
global $fictioneer_story_card_cache;
// Setup
if ( ! $fictioneer_story_card_cache ) {
$fictioneer_story_card_cache = fictioneer_get_story_card_cache();
}
// Look in currently cached cards...
if ( $card = ( $fictioneer_story_card_cache[ $key ] ?? 0 ) ) {
// Remove cached card...
unset( $fictioneer_story_card_cache[ $key ] );
// ... and append at the end
$fictioneer_story_card_cache[ $key ] = $card;
} else {
return null;
}
// Return cached card
return $card;
}
/**
* Purges the entire story card cache
*
* @since 5.22.2
*/
function fictioneer_purge_story_card_cache() {
// Abort if...
if ( ! FICTIONEER_ENABLE_STORY_CARD_CACHING ) {
return;
}
global $fictioneer_story_card_cache;
$fictioneer_story_card_cache = [];
delete_transient( 'fictioneer_card_cache' );
}
/**
* Deletes a specific card from the story card cache
*
* @since 5.22.2
*
* @param int $post_id Post ID of the story.
*/
function fictioneer_delete_cached_story_card( $post_id ) {
// Abort if...
if ( ! FICTIONEER_ENABLE_STORY_CARD_CACHING ) {
return;
}
// Initialize global cache variable
global $fictioneer_story_card_cache;
// Setup
if ( ! $fictioneer_story_card_cache ) {
$fictioneer_story_card_cache = fictioneer_get_story_card_cache();
}
// Find matching keys
foreach ( $fictioneer_story_card_cache as $key => $value ) {
if ( strpos( $key, $post_id . '_' ) === 0 ) {
unset( $fictioneer_story_card_cache[ $key ] );
}
}
}
/**
* Deletes a specific card from the story card cache after update
*
* @since 5.22.3
*
* @param int $post_id The post ID.
*/
function fictioneer_delete_cached_story_card_after_update( $post_id ) {
// Story?
if ( get_post_type( $post_id ) === 'fcn_story' ) {
fictioneer_delete_cached_story_card( $post_id );
return;
}
// Chapter?
if ( get_post_type( $post_id ) === 'fcn_chapter' ) {
$story_id = fictioneer_get_chapter_story_id( $post_id );
if ( $story_id ) {
fictioneer_delete_cached_story_card( $story_id );
}
}
}
add_action( 'save_post', 'fictioneer_delete_cached_story_card_after_update' );
/**
* Deletes a specific card from the story card cache by comment ID
*
* @since 5.22.2
*
* @param int $comment_id ID of the comment belonging to the story
*/
function fictioneer_delete_cached_story_card_by_comment( $comment_id ) {
// Abort if...
if ( ! FICTIONEER_ENABLE_STORY_CARD_CACHING ) {
return;
}
// Setup
$comment = get_comment( $comment_id );
if ( $comment ) {
$story_id = fictioneer_get_chapter_story_id( $comment->comment_post_ID );
if ( $story_id ) {
fictioneer_delete_cached_story_card( $story_id );
}
}
}
add_action( 'wp_insert_comment', 'fictioneer_delete_cached_story_card_by_comment' );
add_action( 'delete_comment', 'fictioneer_delete_cached_story_card_by_comment' );
/**
* Saves the story card cache array as Transient
*
* @since 5.22.2
*/
function fictioneer_save_story_card_cache() {
// Abort if...
if ( ! FICTIONEER_ENABLE_STORY_CARD_CACHING || fictioneer_story_card_cache_lock() ) {
return;
}
// Clear lock
delete_transient( 'fictioneer_story_card_cache_lock' );
// Initialize global cache variable
global $fictioneer_story_card_cache;
if ( ! is_array( $fictioneer_story_card_cache ) ) {
return;
}
// Ensure the limit is respected
$count = count( $fictioneer_story_card_cache );
if ( $count > FICTIONEER_CARD_CACHE_LIMIT ) {
$fictioneer_story_card_cache = array_slice(
$fictioneer_story_card_cache,
$count - FICTIONEER_CARD_CACHE_LIMIT,
FICTIONEER_CARD_CACHE_LIMIT,
true
);
}
// Save Transient
set_transient( 'fictioneer_card_cache', $fictioneer_story_card_cache, FICTIONEER_CARD_CACHE_EXPIRATION_TIME );
}
add_action( 'shutdown', 'fictioneer_save_story_card_cache' );