| 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 |
private static $instance = null; |
| 37 |
|
| 38 |
/** |
| 39 |
* Whether this request already queued a shutdown purge (one per request). |
| 40 |
* |
| 41 |
* @var bool |
| 42 |
*/ |
| 43 |
private $purge_queued = false; |
| 44 |
|
| 45 |
public static function init() { |
| 46 |
if (self::$instance === null) { |
| 47 |
self::$instance = new self(); |
| 48 |
} |
| 49 |
return self::$instance; |
| 50 |
} |
| 51 |
|
| 52 |
private function __construct() { |
| 53 |
foreach ($this->watched_options() as $option_name) { |
| 54 |
add_action('update_option_' . $option_name, array($this, 'queue_purge'), 10, 0); |
| 55 |
add_action('add_option_' . $option_name, array($this, 'queue_purge'), 10, 0); |
| 56 |
} |
| 57 |
add_action(self::DEFERRED_EVENT, array($this, 'purge_page_caches')); |
| 58 |
} |
| 59 |
|
| 60 |
/** |
| 61 |
* Option names whose writes trigger a page-cache purge. |
| 62 |
* |
| 63 |
* Defaults cover every option whose values reach the cached page's inline |
| 64 |
* widget payload: mxchat_options (settings/behavior gates), |
| 65 |
* mxchat_theme_options (active AI theme CSS + bot theme assignments — |
| 66 |
* written only by the MxChat Theme add-on's admin AJAX actions), and |
| 67 |
* mxchat_prompts_options (Pinecone toggle — written only by the prompts |
| 68 |
* autosave). All write sites are admin-action-driven; the debounce |
| 69 |
* absorbs back-to-back writes like apply-and-save. |
| 70 |
* |
| 71 |
* @return string[] |
| 72 |
*/ |
| 73 |
private function watched_options() { |
| 74 |
$defaults = array('mxchat_options', 'mxchat_theme_options', 'mxchat_prompts_options'); |
| 75 |
$options = apply_filters('mxchat_cache_purge_watched_options', $defaults); |
| 76 |
return is_array($options) ? array_filter(array_map('strval', $options)) : $defaults; |
| 77 |
} |
| 78 |
|
| 79 |
/** |
| 80 |
* Coalesce all option writes in this request into one shutdown purge. |
| 81 |
*/ |
| 82 |
public function queue_purge() { |
| 83 |
if ($this->purge_queued) { |
| 84 |
return; |
| 85 |
} |
| 86 |
$this->purge_queued = true; |
| 87 |
add_action('shutdown', array($this, 'run_queued_purge'), 5); |
| 88 |
} |
| 89 |
|
| 90 |
/** |
| 91 |
* Shutdown handler: purge unless one ran inside the debounce window; in |
| 92 |
* that case schedule a single deferred purge so the last write still wins. |
| 93 |
*/ |
| 94 |
public function run_queued_purge() { |
| 95 |
if (get_transient(self::DEBOUNCE_TRANSIENT)) { |
| 96 |
if (!wp_next_scheduled(self::DEFERRED_EVENT)) { |
| 97 |
wp_schedule_single_event(time() + self::DEBOUNCE_SECONDS, self::DEFERRED_EVENT); |
| 98 |
} |
| 99 |
return; |
| 100 |
} |
| 101 |
set_transient(self::DEBOUNCE_TRANSIENT, 1, self::DEBOUNCE_SECONDS); |
| 102 |
$this->purge_page_caches(); |
| 103 |
} |
| 104 |
|
| 105 |
/** |
| 106 |
* Purge every supported page cache via its own public API. Each call is |
| 107 |
* guarded so installs without that plugin skip it silently. |
| 108 |
*/ |
| 109 |
public function purge_page_caches() { |
| 110 |
// WP Rocket — public helper, clears the whole domain's page cache. |
| 111 |
if (function_exists('rocket_clean_domain')) { |
| 112 |
rocket_clean_domain(); |
| 113 |
} |
| 114 |
|
| 115 |
// LiteSpeed Cache — documented third-party purge API. |
| 116 |
if (defined('LSCWP_V') || class_exists('LiteSpeed\Core')) { |
| 117 |
do_action('litespeed_purge_all'); |
| 118 |
} |
| 119 |
|
| 120 |
// W3 Total Cache — page-cache flush helper. |
| 121 |
if (function_exists('w3tc_pgcache_flush')) { |
| 122 |
w3tc_pgcache_flush(); |
| 123 |
} |
| 124 |
|
| 125 |
// WP Super Cache — global cache clear. |
| 126 |
if (function_exists('wp_cache_clear_cache')) { |
| 127 |
wp_cache_clear_cache(); |
| 128 |
} |
| 129 |
|
| 130 |
// FlyingPress — documented programmatic purge (purge_pages clears the |
| 131 |
// HTML page cache without rebuilding optimized assets): |
| 132 |
// https://docs.flyingpress.com/en/articles/11406092-programmatically-purge-and-preload-cache |
| 133 |
if (class_exists('\FlyingPress\Purge') && method_exists('\FlyingPress\Purge', 'purge_pages')) { |
| 134 |
\FlyingPress\Purge::purge_pages(); |
| 135 |
} |
| 136 |
|
| 137 |
// Extension point for CDNs / host caches / custom setups. |
| 138 |
do_action('mxchat_purge_page_caches'); |
| 139 |
} |
| 140 |
} |
| 141 |
|