| 1 |
<?php |
| 2 |
if (!defined('ABSPATH')) { |
| 3 |
exit; |
| 4 |
} |
| 5 |
|
| 6 |
/** |
| 7 |
* MxChat_Cache_Purge — purges known full-page caches when MxChat settings save. |
| 8 |
* |
| 9 |
* Widget settings are printed inline into page HTML (wp_localize_script in |
| 10 |
* class-mxchat-integrator.php), so a full-page cache keeps serving the old |
| 11 |
* snapshot after an admin changes a setting — toggles look broken until the |
| 12 |
* cache is cleared by hand. This module clears the page caches we can reach |
| 13 |
* from PHP the moment mxchat_options is written, using each cache plugin's |
| 14 |
* own public purge API (guarded by existence checks, so absent plugins are |
| 15 |
* a no-op). Companion to the 3.2.6 page-cache compatibility filters in |
| 16 |
* mxchat-basic.php, which keep the chat AJAX endpoints out of those same |
| 17 |
* caches; this handles the cached page HTML itself. |
| 18 |
* |
| 19 |
* Host-level caches and CDNs (Cloudflare APO etc.) cannot be purged from |
| 20 |
* PHP — for those, the widget re-fetches its behavior settings when opened |
| 21 |
* (see MxChat_Integrator::get_dynamic_widget_settings()). |
| 22 |
* |
| 23 |
* Debounce: the admin screen autosaves per field, so a settings session can |
| 24 |
* write mxchat_options many times in a minute. We purge at most once per |
| 25 |
* DEBOUNCE_SECONDS; writes that land inside the window schedule one deferred |
| 26 |
* purge via wp-cron so the final state always gets flushed. |
| 27 |
* |
| 28 |
* @since 3.2.9 (plan-32db95) |
| 29 |
*/ |
| 30 |
class MxChat_Cache_Purge { |
| 31 |
|
| 32 |
const DEBOUNCE_TRANSIENT = 'mxchat_cache_purge_debounce'; |
| 33 |
const DEBOUNCE_SECONDS = 30; |
| 34 |
const DEFERRED_EVENT = 'mxchat_deferred_cache_purge'; |
| 35 |
|
| 36 |
/** |
| 37 |
* Owner switch (plan b76ed6, Support Ticket 468: a 1,400-page FlyingPress |
| 38 |
* site lost its whole preload on every settings save). Standalone option, |
| 39 |
* 'on' / 'off'; absent means ON so existing sites keep today's behaviour. |
| 40 |
* Rendered on Settings → Optimization → Page Cache Compatibility and saved |
| 41 |
* by the mxchat_cache_purge_enabled case in class-ajax-handler.php. |
| 42 |
* |
| 43 |
* The switch is read when a watched option is actually written (and when |
| 44 |
* a deferred purge fires), not at plugin load: the hooks below cost |
| 45 |
* nothing until a save happens, and a site that never touches the switch |
| 46 |
* never pays a get_option() for it on a visitor request. |
| 47 |
*/ |
| 48 |
const OPTION_NAME = 'mxchat_cache_purge_enabled'; |
| 49 |
|
| 50 |
private static $instance = null; |
| 51 |
|
| 52 |
/** |
| 53 |
* Whether this request already queued a shutdown purge (one per request). |
| 54 |
* |
| 55 |
* @var bool |
| 56 |
*/ |
| 57 |
private $purge_queued = false; |
| 58 |
|
| 59 |
public static function init() { |
| 60 |
if (self::$instance === null) { |
| 61 |
self::$instance = new self(); |
| 62 |
} |
| 63 |
return self::$instance; |
| 64 |
} |
| 65 |
|
| 66 |
/** |
| 67 |
* Whether MxChat clears page caches when its settings are saved. |
| 68 |
* Default ON; only an explicit 'off' switches it off. |
| 69 |
* |
| 70 |
* @return bool |
| 71 |
*/ |
| 72 |
public static function is_enabled() { |
| 73 |
return get_option(self::OPTION_NAME, 'on') !== 'off'; |
| 74 |
} |
| 75 |
|
| 76 |
private function __construct() { |
| 77 |
foreach ($this->watched_options() as $option_name) { |
| 78 |
add_action('update_option_' . $option_name, array($this, 'queue_purge'), 10, 0); |
| 79 |
add_action('add_option_' . $option_name, array($this, 'queue_purge'), 10, 0); |
| 80 |
} |
| 81 |
add_action(self::DEFERRED_EVENT, array($this, 'purge_page_caches')); |
| 82 |
} |
| 83 |
|
| 84 |
/** |
| 85 |
* Option names whose writes trigger a page-cache purge. |
| 86 |
* |
| 87 |
* Defaults cover every option whose values reach the cached page's inline |
| 88 |
* widget payload: mxchat_options (settings/behavior gates), |
| 89 |
* mxchat_theme_options (active AI theme CSS + bot theme assignments — |
| 90 |
* written only by the MxChat Theme add-on's admin AJAX actions), and |
| 91 |
* mxchat_prompts_options (Pinecone toggle — written only by the prompts |
| 92 |
* autosave). All write sites are admin-action-driven; the debounce |
| 93 |
* absorbs back-to-back writes like apply-and-save. |
| 94 |
* |
| 95 |
* @return string[] |
| 96 |
*/ |
| 97 |
private function watched_options() { |
| 98 |
$defaults = array('mxchat_options', 'mxchat_theme_options', 'mxchat_prompts_options'); |
| 99 |
$options = apply_filters('mxchat_cache_purge_watched_options', $defaults); |
| 100 |
return is_array($options) ? array_filter(array_map('strval', $options)) : $defaults; |
| 101 |
} |
| 102 |
|
| 103 |
/** |
| 104 |
* Coalesce all option writes in this request into one shutdown purge. |
| 105 |
*/ |
| 106 |
public function queue_purge() { |
| 107 |
if ($this->purge_queued || !self::is_enabled()) { |
| 108 |
return; |
| 109 |
} |
| 110 |
$this->purge_queued = true; |
| 111 |
add_action('shutdown', array($this, 'run_queued_purge'), 5); |
| 112 |
} |
| 113 |
|
| 114 |
/** |
| 115 |
* Shutdown handler: purge unless one ran inside the debounce window; in |
| 116 |
* that case schedule a single deferred purge so the last write still wins. |
| 117 |
*/ |
| 118 |
public function run_queued_purge() { |
| 119 |
if (get_transient(self::DEBOUNCE_TRANSIENT)) { |
| 120 |
if (!wp_next_scheduled(self::DEFERRED_EVENT)) { |
| 121 |
wp_schedule_single_event(time() + self::DEBOUNCE_SECONDS, self::DEFERRED_EVENT); |
| 122 |
} |
| 123 |
return; |
| 124 |
} |
| 125 |
set_transient(self::DEBOUNCE_TRANSIENT, 1, self::DEBOUNCE_SECONDS); |
| 126 |
$this->purge_page_caches(); |
| 127 |
} |
| 128 |
|
| 129 |
/** |
| 130 |
* Purge every supported page cache via its own public API. Each call is |
| 131 |
* guarded so installs without that plugin skip it silently. |
| 132 |
*/ |
| 133 |
public function purge_page_caches() { |
| 134 |
// The owner may have switched the purge off after a deferred purge was |
| 135 |
// scheduled; off means off. |
| 136 |
if (!self::is_enabled()) { |
| 137 |
return; |
| 138 |
} |
| 139 |
|
| 140 |
// WP Rocket — public helper, clears the whole domain's page cache. |
| 141 |
if (function_exists('rocket_clean_domain')) { |
| 142 |
rocket_clean_domain(); |
| 143 |
} |
| 144 |
|
| 145 |
// LiteSpeed Cache — documented third-party purge API. |
| 146 |
if (defined('LSCWP_V') || class_exists('LiteSpeed\Core')) { |
| 147 |
do_action('litespeed_purge_all'); |
| 148 |
} |
| 149 |
|
| 150 |
// W3 Total Cache — page-cache flush helper. |
| 151 |
if (function_exists('w3tc_pgcache_flush')) { |
| 152 |
w3tc_pgcache_flush(); |
| 153 |
} |
| 154 |
|
| 155 |
// WP Super Cache — global cache clear. |
| 156 |
if (function_exists('wp_cache_clear_cache')) { |
| 157 |
wp_cache_clear_cache(); |
| 158 |
} |
| 159 |
|
| 160 |
// FlyingPress — documented programmatic purge (purge_pages clears the |
| 161 |
// HTML page cache without rebuilding optimized assets): |
| 162 |
// https://docs.flyingpress.com/en/articles/11406092-programmatically-purge-and-preload-cache |
| 163 |
if (class_exists('\FlyingPress\Purge') && method_exists('\FlyingPress\Purge', 'purge_pages')) { |
| 164 |
\FlyingPress\Purge::purge_pages(); |
| 165 |
} |
| 166 |
|
| 167 |
// Extension point for CDNs / host caches / custom setups. |
| 168 |
do_action('mxchat_purge_page_caches'); |
| 169 |
} |
| 170 |
} |
| 171 |
|