PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / trunk
Search Atlas SEO – OTTO AI SEO Automation for WordPress vtrunk
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / includes / class-metasync-cache-purge.php

class-metasync-cache-purge.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress trunk, at includes/class-metasync-cache-purge.php

1,046 lines 34.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MetaSync Universal Cache Purge Handler
4 *
5 * Integrates with all major WordPress cache plugins to ensure
6 * changes made by MetaSync and OTTO are immediately visible
7 *
8 * @package Metasync
9 * @subpackage Metasync/includes
10 * @since 1.0.0
11 */
12
13 // Prevent direct access
14 if (!defined('ABSPATH')) {
15 exit;
16 }
17
18 class Metasync_Cache_Purge
19 {
20 /**
21 * Singleton instance
22 */
23 private static $instance = null;
24
25 /**
26 * Get singleton instance
27 */
28 public static function get_instance()
29 {
30 if (null === self::$instance) {
31 self::$instance = new self();
32 }
33 return self::$instance;
34 }
35
36 /**
37 * Constructor - private to enforce singleton
38 */
39 private function __construct()
40 {
41 // Constructor is private
42 }
43
44 /**
45 * Clear ALL cache from ALL detected cache plugins
46 *
47 * @param string $source Optional source identifier for logging
48 * @param array $args Optional arguments:
49 * 'post_ids' (int[]) — when provided, only those posts are
50 * removed from the object cache (targeted mode).
51 * When absent, falls back to wp_cache_flush().
52 * @return array Results of cache clearing attempts
53 */
54 public function clear_all_caches($source = 'metasync', $args = array())
55 {
56 $results = array(
57 'cleared' => array(),
58 'failed' => array(),
59 'not_active' => array()
60 );
61
62 $post_ids = !empty($args['post_ids']) && is_array($args['post_ids'])
63 ? array_map('intval', $args['post_ids'])
64 : array();
65
66 // Clear WordPress built-in cache
67 if ($this->clear_wordpress_cache($post_ids)) {
68 $results['cleared'][] = 'WordPress Object Cache';
69 }
70
71 // Clear WP Rocket
72 if ($this->is_plugin_active('wp-rocket/wp-rocket.php')) {
73 if ($this->clear_wp_rocket_cache()) {
74 $results['cleared'][] = 'WP Rocket';
75 } else {
76 $results['failed'][] = 'WP Rocket';
77 }
78 } else {
79 $results['not_active'][] = 'WP Rocket';
80 }
81
82 // Clear LiteSpeed Cache
83 if ($this->is_plugin_active('litespeed-cache/litespeed-cache.php')) {
84 if ($this->clear_litespeed_cache()) {
85 $results['cleared'][] = 'LiteSpeed Cache';
86 } else {
87 $results['failed'][] = 'LiteSpeed Cache';
88 }
89 } else {
90 $results['not_active'][] = 'LiteSpeed Cache';
91 }
92
93 // Clear W3 Total Cache
94 if ($this->is_plugin_active('w3-total-cache/w3-total-cache.php')) {
95 if ($this->clear_w3_total_cache()) {
96 $results['cleared'][] = 'W3 Total Cache';
97 } else {
98 $results['failed'][] = 'W3 Total Cache';
99 }
100 } else {
101 $results['not_active'][] = 'W3 Total Cache';
102 }
103
104 // Clear WP Super Cache
105 if ($this->is_plugin_active('wp-super-cache/wp-cache.php')) {
106 if ($this->clear_wp_super_cache()) {
107 $results['cleared'][] = 'WP Super Cache';
108 } else {
109 $results['failed'][] = 'WP Super Cache';
110 }
111 } else {
112 $results['not_active'][] = 'WP Super Cache';
113 }
114
115 // Clear WP Fastest Cache
116 if ($this->is_plugin_active('wp-fastest-cache/wpFastestCache.php')) {
117 if ($this->clear_wp_fastest_cache()) {
118 $results['cleared'][] = 'WP Fastest Cache';
119 } else {
120 $results['failed'][] = 'WP Fastest Cache';
121 }
122 } else {
123 $results['not_active'][] = 'WP Fastest Cache';
124 }
125
126 // Clear Cache Enabler
127 if ($this->is_plugin_active('cache-enabler/cache-enabler.php')) {
128 if ($this->clear_cache_enabler()) {
129 $results['cleared'][] = 'Cache Enabler';
130 } else {
131 $results['failed'][] = 'Cache Enabler';
132 }
133 } else {
134 $results['not_active'][] = 'Cache Enabler';
135 }
136
137 // Clear Hummingbird Cache
138 if ($this->is_plugin_active('hummingbird-performance/wp-hummingbird.php')) {
139 if ($this->clear_hummingbird_cache()) {
140 $results['cleared'][] = 'Hummingbird';
141 } else {
142 $results['failed'][] = 'Hummingbird';
143 }
144 } else {
145 $results['not_active'][] = 'Hummingbird';
146 }
147
148 // Clear Autoptimize Cache
149 if ($this->is_plugin_active('autoptimize/autoptimize.php')) {
150 if ($this->clear_autoptimize_cache()) {
151 $results['cleared'][] = 'Autoptimize';
152 } else {
153 $results['failed'][] = 'Autoptimize';
154 }
155 } else {
156 $results['not_active'][] = 'Autoptimize';
157 }
158
159 // Clear SG Optimizer (SiteGround)
160 if ($this->is_plugin_active('sg-cachepress/sg-cachepress.php')) {
161 if ($this->clear_sg_optimizer_cache()) {
162 $results['cleared'][] = 'SG Optimizer';
163 } else {
164 $results['failed'][] = 'SG Optimizer';
165 }
166 } else {
167 $results['not_active'][] = 'SG Optimizer';
168 }
169
170 // Clear Comet Cache
171 if ($this->is_plugin_active('comet-cache/comet-cache.php')) {
172 if ($this->clear_comet_cache()) {
173 $results['cleared'][] = 'Comet Cache';
174 } else {
175 $results['failed'][] = 'Comet Cache';
176 }
177 } else {
178 $results['not_active'][] = 'Comet Cache';
179 }
180
181 // Clear Swift Performance
182 if ($this->is_plugin_active('swift-performance-lite/performance.php') ||
183 $this->is_plugin_active('swift-performance/performance.php')) {
184 if ($this->clear_swift_performance_cache()) {
185 $results['cleared'][] = 'Swift Performance';
186 } else {
187 $results['failed'][] = 'Swift Performance';
188 }
189 } else {
190 $results['not_active'][] = 'Swift Performance';
191 }
192
193 // Clear NitroPack Cache
194 if ($this->is_plugin_active('nitropack/main.php')) {
195 if ($this->clear_nitropack_cache()) {
196 $results['cleared'][] = 'NitroPack';
197 } else {
198 $results['failed'][] = 'NitroPack';
199 }
200 } else {
201 $results['not_active'][] = 'NitroPack';
202 }
203
204 // Clear Kinsta Cache (hosting-level) — respects the "Hosting Cache Integration" setting
205 $hosting_settings = get_option('metasync_hosting_cache_options', array('kinsta_enabled' => true, 'wpengine_enabled' => true));
206 if (!isset($hosting_settings['kinsta_enabled'])) {
207 $hosting_settings['kinsta_enabled'] = true;
208 }
209 if (!isset($hosting_settings['wpengine_enabled'])) {
210 $hosting_settings['wpengine_enabled'] = true;
211 }
212
213 if (!empty($hosting_settings['kinsta_enabled'])) {
214 if (class_exists('KinstaCache')) {
215 if ($this->clear_kinsta_cache()) {
216 $results['cleared'][] = 'Kinsta Cache';
217 } else {
218 $results['failed'][] = 'Kinsta Cache';
219 }
220 } else {
221 $results['not_active'][] = 'Kinsta Cache';
222 }
223 }
224
225 // Clear WP Engine Cache (hosting-level) — respects the "Hosting Cache Integration" setting
226 if (!empty($hosting_settings['wpengine_enabled'])) {
227 if (class_exists('WpeCommon')) {
228 if ($this->clear_wpengine_cache()) {
229 $results['cleared'][] = 'WP Engine Cache';
230 } else {
231 $results['failed'][] = 'WP Engine Cache';
232 }
233 } else {
234 $results['not_active'][] = 'WP Engine Cache';
235 }
236 }
237
238 // Clear MetaSync Minification Cache
239 if (class_exists('Metasync_Minification_Cache')) {
240 try {
241 Metasync_Minification_Cache::purge_all();
242 $results['cleared'][] = 'MetaSync Minification Cache';
243 } catch (\Throwable $e) {
244 $results['failed'][] = 'MetaSync Minification Cache';
245 }
246 }
247
248 return $results;
249 }
250
251 /**
252 * Clear cache for a specific URL or post
253 *
254 * @param string|int $url_or_post_id URL or Post ID
255 * @return bool Success status
256 */
257 public function clear_url_cache($url_or_post_id)
258 {
259 $success = false;
260
261 // Determine if it's a URL or Post ID
262 if (is_numeric($url_or_post_id)) {
263 $post_id = intval($url_or_post_id);
264 $url = get_permalink($post_id);
265 } else {
266 $url = $url_or_post_id;
267 // Strip tracking query parameters before resolving post ID so
268 // url_to_postid() receives a clean canonical URL
269 $url = self::normalize_url($url);
270 $post_id = url_to_postid($url);
271 }
272
273 // WP Rocket - Clear specific URL.
274 // Prefer rocket_clean_url() (URL-based, WP Rocket 3+) so custom post types
275 // and URLs that url_to_postid() can't resolve are still purged correctly.
276 if ($this->is_plugin_active('wp-rocket/wp-rocket.php')) {
277 try {
278 if ($url && function_exists('rocket_clean_url')) {
279 rocket_clean_url($url);
280 $success = true;
281 } elseif ($post_id && function_exists('rocket_clean_post')) {
282 rocket_clean_post($post_id);
283 $success = true;
284 }
285 } catch (\Throwable $e) {
286 error_log('MetaSync: WP Rocket per-URL purge failed - ' . $e->getMessage());
287 }
288 }
289
290 // LiteSpeed Cache - Purge specific URL
291 if ($this->is_plugin_active('litespeed-cache/litespeed-cache.php')) {
292 try {
293 if ($post_id && class_exists('LiteSpeed\\Purge')) {
294 do_action('litespeed_purge_post', $post_id);
295 $success = true;
296 }
297 } catch (\Throwable $e) {
298 error_log('MetaSync: LiteSpeed per-URL purge failed - ' . $e->getMessage());
299 }
300 }
301
302 // W3 Total Cache - Flush specific post
303 if ($this->is_plugin_active('w3-total-cache/w3-total-cache.php')) {
304 try {
305 if ($post_id && function_exists('w3tc_flush_post')) {
306 w3tc_flush_post($post_id);
307 $success = true;
308 }
309 } catch (\Throwable $e) {
310 error_log('MetaSync: W3 Total Cache per-URL purge failed - ' . $e->getMessage());
311 }
312 }
313
314 // NitroPack - Purge specific URL cache (both local and remote cache)
315 if ($this->is_plugin_active('nitropack/main.php')) {
316 try {
317 if ($url && function_exists('nitropack_sdk_purge')) {
318 if (nitropack_sdk_purge($url, NULL, 'MetaSync OTTO cache clear', \NitroPack\SDK\PurgeType::COMPLETE)) {
319 $success = true;
320 }
321 }
322 } catch (\Throwable $e) {
323 error_log('MetaSync: NitroPack per-URL purge failed - ' . $e->getMessage());
324 }
325 }
326
327 // WP Super Cache - Purge specific URL
328 if ($this->is_plugin_active('wp-super-cache/wp-cache.php')) {
329 try {
330 if ($url && function_exists('wpsc_delete_url_cache')) {
331 wpsc_delete_url_cache($url);
332 $success = true;
333 } elseif ($url && function_exists('prune_super_cache')) {
334 // Fallback: derive cache file path from the URL
335 $url_path = wp_parse_url($url, PHP_URL_PATH);
336 if ($url_path) {
337 $cache_path = trailingslashit(WP_CONTENT_DIR . '/cache/supercache/' . wp_parse_url($url, PHP_URL_HOST) . $url_path);
338 prune_super_cache($cache_path, true);
339 $success = true;
340 }
341 }
342 } catch (\Throwable $e) {
343 error_log('MetaSync: WP Super Cache per-URL purge failed - ' . $e->getMessage());
344 }
345 }
346
347 // WP Fastest Cache - Purge specific post cache
348 // singleDeleteCache($comment_id, $post_id) requires a post ID, not URL
349 if ($this->is_plugin_active('wp-fastest-cache/wpFastestCache.php')) {
350 try {
351 $wpfc_purged = false;
352 if ($post_id && class_exists('WpFastestCache')) {
353 $wpfc = new \WpFastestCache();
354 if (method_exists($wpfc, 'singleDeleteCache')) {
355 $wpfc->singleDeleteCache(false, $post_id);
356 $wpfc_purged = true;
357 $success = true;
358 }
359 }
360 if (!$wpfc_purged && $url) {
361 do_action('wpfc_clear_post_cache_by_url', $url);
362 $success = true;
363 }
364 } catch (\Throwable $e) {
365 error_log('MetaSync: WP Fastest Cache per-URL purge failed - ' . $e->getMessage());
366 }
367 }
368
369 // Cache Enabler - Purge specific URL
370 if ($this->is_plugin_active('cache-enabler/cache-enabler.php')) {
371 try {
372 if ($url) {
373 do_action('cache_enabler_clear_page_cache_by_url', $url);
374 $success = true;
375 }
376 } catch (\Throwable $e) {
377 error_log('MetaSync: Cache Enabler per-URL purge failed - ' . $e->getMessage());
378 }
379 }
380
381 // Hummingbird - Purge specific URL/post
382 if ($this->is_plugin_active('hummingbird-performance/wp-hummingbird.php')) {
383 try {
384 $hb_purged = false;
385 if ($post_id) {
386 do_action('wphb_clear_page_cache', $post_id);
387 $hb_purged = true;
388 $success = true;
389 }
390 if (!$hb_purged && class_exists('Hummingbird\\WP_Hummingbird')) {
391 $modules = \Hummingbird\WP_Hummingbird::get_instance()->core->modules;
392 if (isset($modules['page_cache']) && method_exists($modules['page_cache'], 'clear_cache')) {
393 $modules['page_cache']->clear_cache();
394 $success = true;
395 }
396 }
397 } catch (\Throwable $e) {
398 error_log('MetaSync: Hummingbird per-URL purge failed - ' . $e->getMessage());
399 }
400 }
401
402 // SG Optimizer - Purge specific URL
403 if ($this->is_plugin_active('sg-cachepress/sg-cachepress.php')) {
404 try {
405 if ($url && function_exists('sg_cachepress_purge_cache')) {
406 sg_cachepress_purge_cache($url);
407 $success = true;
408 } else {
409 do_action('sg_cachepress_purge_cache');
410 $success = true;
411 }
412 } catch (\Throwable $e) {
413 error_log('MetaSync: SG Optimizer per-URL purge failed - ' . $e->getMessage());
414 }
415 }
416
417 // Comet Cache - Purge specific URL/post cache
418 if ($this->is_plugin_active('comet-cache/comet-cache.php') && class_exists('comet_cache')) {
419 try {
420 if ($url && method_exists('comet_cache', 'clearUrl')) {
421 comet_cache::clearUrl($url);
422 $success = true;
423 } elseif ($post_id && method_exists('comet_cache', 'clearPost')) {
424 comet_cache::clearPost($post_id);
425 $success = true;
426 }
427 } catch (\Throwable $e) {
428 error_log('MetaSync: Comet Cache per-URL purge failed - ' . $e->getMessage());
429 }
430 }
431
432 // Swift Performance - Purge specific post cache
433 if ($this->is_plugin_active('swift-performance-lite/performance.php') ||
434 $this->is_plugin_active('swift-performance/performance.php')) {
435 try {
436 if ($post_id && class_exists('Swift_Performance_Cache')) {
437 Swift_Performance_Cache::clear_post_cache($post_id);
438 $success = true;
439 }
440 } catch (\Throwable $e) {
441 error_log('MetaSync: Swift Performance per-URL purge failed - ' . $e->getMessage());
442 }
443 }
444
445 // Autoptimize - Skipped for per-URL purge (only caches JS/CSS assets, not page HTML)
446
447 // Kinsta - Purge specific URL (hosting-level cache, no plugin check needed)
448 if ($url && class_exists('KinstaCache')) {
449 try {
450 KinstaCache::get_instance()->kinsta_cache_purge_single_url($url);
451 $success = true;
452 } catch (\Throwable $e) {
453 error_log('MetaSync: Kinsta per-URL purge failed - ' . $e->getMessage());
454 }
455 }
456
457 // WP Engine - Purge specific URL (hosting-level cache, no plugin check needed)
458 if (class_exists('WpeCommon') && $url) {
459 try {
460 if (method_exists('WpeCommon', 'purge_url')) {
461 WpeCommon::purge_url($url);
462 $success = true;
463 } elseif (method_exists('WpeCommon', 'purge_varnish_cache_for_url')) {
464 WpeCommon::purge_varnish_cache_for_url($url);
465 $success = true;
466 }
467 } catch (\Throwable $e) {
468 error_log('MetaSync: WP Engine per-URL purge failed - ' . $e->getMessage());
469 }
470 }
471
472 return $success;
473 }
474
475 /**
476 * Clear WordPress built-in object cache.
477 *
478 * When $post_ids is provided and the metasync_targeted_object_cache option is
479 * enabled (default: true), only those posts are evicted from the object cache.
480 * This prevents destroying cached data for the entire site when only a handful
481 * of OTTO pages were updated.
482 *
483 * Falls back to wp_cache_flush() when no post IDs are given, targeted mode is
484 * disabled, or anything goes wrong.
485 *
486 * @param int[] $post_ids Optional list of post IDs to evict (targeted mode).
487 */
488 private function clear_wordpress_cache($post_ids = array())
489 {
490 try {
491 $targeted_enabled = get_option('metasync_targeted_object_cache', '1') === '1';
492
493 if (!empty($post_ids) && $targeted_enabled) {
494 foreach ($post_ids as $post_id) {
495 wp_cache_delete($post_id, 'posts');
496 wp_cache_delete($post_id, 'post_meta');
497 clean_post_cache($post_id);
498 }
499 } else {
500 wp_cache_flush();
501 }
502
503 return true;
504 } catch (\Throwable $e) {
505 $this->log_cache_error('WordPress', $e);
506 return false;
507 }
508 }
509
510 /**
511 * Clear WP Rocket cache
512 */
513 private function clear_wp_rocket_cache()
514 {
515 try {
516 // Clear all cache
517 if (function_exists('rocket_clean_domain')) {
518 rocket_clean_domain();
519 return true;
520 }
521
522 // Fallback: Clear minified CSS/JS
523 if (function_exists('rocket_clean_minify')) {
524 rocket_clean_minify();
525 }
526
527 // Clear cache via action hook
528 do_action('rocket_clean_domain');
529
530 return true;
531 } catch (\Throwable $e) {
532 $this->log_cache_error('WP Rocket', $e);
533 return false;
534 }
535 }
536
537 /**
538 * Clear LiteSpeed Cache
539 */
540 private function clear_litespeed_cache()
541 {
542 try {
543 // Method 1: Use LiteSpeed purge action
544 do_action('litespeed_purge_all');
545
546 // Method 2: Use LiteSpeed class if available
547 if (class_exists('LiteSpeed\Purge')) {
548 \LiteSpeed\Purge::purge_all();
549 }
550
551 // Method 3: Set header for LiteSpeed server
552 if (!headers_sent()) {
553 header('X-LiteSpeed-Purge: *');
554 }
555
556 return true;
557 } catch (\Throwable $e) {
558 $this->log_cache_error('LiteSpeed', $e);
559 return false;
560 }
561 }
562
563 /**
564 * Clear W3 Total Cache
565 */
566 private function clear_w3_total_cache()
567 {
568 try {
569 // Method 1: Use W3TC functions
570 if (function_exists('w3tc_flush_all')) {
571 w3tc_flush_all();
572 return true;
573 }
574
575 // Method 2: Use W3TC class
576 if (class_exists('W3_Plugin_TotalCache')) {
577 $w3_plugin_totalcache = w3_instance('W3_Plugin_TotalCache');
578 if (method_exists($w3_plugin_totalcache, 'flush_all')) {
579 $w3_plugin_totalcache->flush_all();
580 return true;
581 }
582 }
583
584 // Method 3: Direct cache directory deletion
585 if (function_exists('w3tc_pgcache_flush')) {
586 w3tc_pgcache_flush();
587 }
588 if (function_exists('w3tc_dbcache_flush')) {
589 w3tc_dbcache_flush();
590 }
591 if (function_exists('w3tc_minify_flush')) {
592 w3tc_minify_flush();
593 }
594 if (function_exists('w3tc_objectcache_flush')) {
595 w3tc_objectcache_flush();
596 }
597
598 return true;
599 } catch (\Throwable $e) {
600 $this->log_cache_error('W3 Total Cache', $e);
601 return false;
602 }
603 }
604
605 /**
606 * Clear WP Super Cache
607 */
608 private function clear_wp_super_cache()
609 {
610 try {
611 global $file_prefix;
612
613 if (function_exists('wp_cache_clean_cache')) {
614 wp_cache_clean_cache($file_prefix, true);
615 return true;
616 }
617
618 // Alternative method
619 if (function_exists('wp_cache_clear_cache')) {
620 wp_cache_clear_cache();
621 return true;
622 }
623
624 // Manual deletion
625 if (function_exists('prune_super_cache')) {
626 prune_super_cache(WP_CONTENT_DIR . '/cache/', true);
627 return true;
628 }
629
630 return false;
631 } catch (\Throwable $e) {
632 $this->log_cache_error('WP Super Cache', $e);
633 return false;
634 }
635 }
636
637 /**
638 * Clear WP Fastest Cache
639 */
640 private function clear_wp_fastest_cache()
641 {
642 try {
643 if (class_exists('WpFastestCache')) {
644 $wpfc = new WpFastestCache();
645 if (method_exists($wpfc, 'deleteCache')) {
646 $wpfc->deleteCache(true);
647 return true;
648 }
649 }
650
651 // Try action hook
652 do_action('wpfc_clear_all_cache');
653
654 return true;
655 } catch (\Throwable $e) {
656 $this->log_cache_error('WP Fastest Cache', $e);
657 return false;
658 }
659 }
660
661 /**
662 * Clear Cache Enabler
663 */
664 private function clear_cache_enabler()
665 {
666 try {
667 if (class_exists('Cache_Enabler')) {
668 if (method_exists('Cache_Enabler', 'clear_complete_cache')) {
669 \Cache_Enabler::clear_complete_cache();
670 return true;
671 }
672 }
673
674 // Try action hook
675 do_action('cache_enabler_clear_complete_cache');
676
677 return true;
678 } catch (\Throwable $e) {
679 $this->log_cache_error('Cache Enabler', $e);
680 return false;
681 }
682 }
683
684 /**
685 * Clear Hummingbird Cache
686 */
687 private function clear_hummingbird_cache()
688 {
689 try {
690 if (class_exists('Hummingbird\\WP_Hummingbird')) {
691 $modules = \Hummingbird\WP_Hummingbird::get_instance()->core->modules;
692 if (isset($modules['page_cache'])) {
693 $modules['page_cache']->clear_cache();
694 return true;
695 }
696 }
697
698 // Try action hook
699 do_action('wphb_clear_page_cache');
700
701 return true;
702 } catch (\Throwable $e) {
703 $this->log_cache_error('Hummingbird', $e);
704 return false;
705 }
706 }
707
708 /**
709 * Clear Autoptimize Cache
710 */
711 private function clear_autoptimize_cache()
712 {
713 try {
714 if (class_exists('autoptimizeCache')) {
715 autoptimizeCache::clearall();
716 return true;
717 }
718
719 // Try action hook
720 do_action('autoptimize_action_cachepurged');
721
722 return true;
723 } catch (\Throwable $e) {
724 $this->log_cache_error('Autoptimize', $e);
725 return false;
726 }
727 }
728
729 /**
730 * Clear SG Optimizer (SiteGround) Cache
731 */
732 private function clear_sg_optimizer_cache()
733 {
734 try {
735 if (function_exists('sg_cachepress_purge_cache')) {
736 sg_cachepress_purge_cache();
737 return true;
738 }
739
740 // Try action hook
741 do_action('sg_cachepress_purge_cache');
742
743 return true;
744 } catch (\Throwable $e) {
745 $this->log_cache_error('SG Optimizer', $e);
746 return false;
747 }
748 }
749
750 /**
751 * Clear Comet Cache
752 */
753 private function clear_comet_cache()
754 {
755 try {
756 if (class_exists('comet_cache')) {
757 comet_cache::clear();
758 return true;
759 }
760
761 // Try action hook
762 do_action('comet_cache_clear');
763
764 return true;
765 } catch (\Throwable $e) {
766 $this->log_cache_error('Comet Cache', $e);
767 return false;
768 }
769 }
770
771 /**
772 * Clear Swift Performance Cache
773 */
774 private function clear_swift_performance_cache()
775 {
776 try {
777 if (class_exists('Swift_Performance_Cache')) {
778 Swift_Performance_Cache::clear_all_cache();
779 return true;
780 }
781
782 // Try action hook
783 do_action('swift_performance_clear_all_cache');
784
785 return true;
786 } catch (\Throwable $e) {
787 $this->log_cache_error('Swift Performance', $e);
788 return false;
789 }
790 }
791
792 /**
793 * Clear Kinsta hosting-level full-page cache (entire site)
794 */
795 private function clear_kinsta_cache()
796 {
797 try {
798 KinstaCache::get_instance()->kinsta_cache_purge_full();
799 return true;
800 } catch (\Throwable $e) {
801 $this->log_cache_error('Kinsta', $e);
802 return false;
803 }
804 }
805
806 /**
807 * Clear WP Engine hosting-level cache (Varnish + memcached)
808 */
809 private function clear_wpengine_cache()
810 {
811 try {
812 WpeCommon::purge_varnish_cache();
813 WpeCommon::purge_memcached();
814 return true;
815 } catch (\Throwable $e) {
816 $this->log_cache_error('WP Engine', $e);
817 return false;
818 }
819 }
820
821 /**
822 * Clear NitroPack full-site cache.
823 *
824 * nitropack_purge(NULL, NULL, $reason) is the documented way to purge the
825 * entire NitroPack cache (all three parameters NULL = full site purge).
826 * Falls back to nitropack_sdk_purge() with no URL/tag if the higher-level
827 * wrapper is unavailable (older plugin versions).
828 *
829 * @see https://wordpress.org/support/topic/custom-caching-code-compatibility-with-nitropack/
830 */
831 private function clear_nitropack_cache()
832 {
833 try {
834 if (function_exists('nitropack_purge')) {
835 nitropack_purge(null, null, 'MetaSync full cache clear');
836 return true;
837 }
838
839 // Fallback: lower-level SDK function (same behaviour when url+tag are NULL)
840 if (function_exists('nitropack_sdk_purge')) {
841 nitropack_sdk_purge(null, null, 'MetaSync full cache clear');
842 return true;
843 }
844
845 return false;
846 } catch (\Throwable $e) {
847 $this->log_cache_error('NitroPack', $e);
848 return false;
849 }
850 }
851
852 /**
853 * Static wrapper: clear all caches with try-catch
854 *
855 * @param string $source Source identifier for logging
856 */
857 public static function purge_all($source = 'metasync')
858 {
859 try {
860 self::get_instance()->clear_all_caches($source);
861 } catch (\Throwable $e) {
862 error_log('MetaSync: Cache purge failed (' . $source . ') - ' . $e->getMessage());
863 }
864 }
865
866 /**
867 * Static wrapper: clear cache for a single URL with try-catch
868 *
869 * @param string $url URL to purge
870 */
871 public static function purge_single_url($url)
872 {
873 try {
874 self::get_instance()->clear_url_cache($url);
875 } catch (\Throwable $e) {
876 error_log('MetaSync: URL cache purge failed - ' . $e->getMessage());
877 }
878 }
879
880 /**
881 * Warm cache for multiple URLs after a purge.
882 *
883 * Sends anonymous fire-and-forget requests so that our code — with fresh OTTO
884 * data already in transients and post meta — is the first to populate each URL
885 * in the hosting cache. Without this, the first external visitor (or the host's
886 * own cache warmer) could re-cache a stale version before OTTO SSR has run.
887 *
888 * Capped at $max URLs per call to prevent excessive server load on large batches.
889 *
890 * @param array $urls List of URLs to warm
891 * @param int $max Maximum URLs to warm per invocation (default 10)
892 */
893 public static function warm_urls(array $urls, $max = 10)
894 {
895 $count = 0;
896 foreach ($urls as $url) {
897 if (++$count > $max) {
898 break;
899 }
900 self::warm_url($url);
901 }
902 }
903
904 /**
905 * Send a single anonymous, non-blocking HTTP GET to a URL to prime the cache.
906 *
907 * The request carries no auth cookies, so the hosting layer (Kinsta, WP Engine, etc.)
908 * treats it as an anonymous visitor and caches the PHP-rendered response.
909 * OTTO SSR runs during this request and modifies the HTML before it is stored.
910 *
911 * Uses blocking=false so the pixel webhook returns immediately; the host
912 * continues processing the request asynchronously.
913 *
914 * @param string $url URL to warm
915 */
916 public static function warm_url($url)
917 {
918 if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
919 return;
920 }
921
922 // Strip tracking query parameters so cache warming targets the canonical URL
923 $url = self::normalize_url($url);
924
925 wp_remote_get($url, array(
926 'blocking' => false, // Fire-and-forget: don't wait for response
927 'timeout' => 3, // 3s to establish connection + send headers reliably
928 // 0.01s was too short: TCP handshake could fail on loaded servers,
929 // causing warm requests to silently drop before the request was sent.
930 // blocking=false means we still don't wait for the response body.
931 'redirection' => 0, // No redirect following needed
932 'headers' => array(
933 'X-MetaSync-Cache-Warm' => '1',
934 'User-Agent' => 'MetaSync-Cache-Warmer/1.0',
935 ),
936 'cookies' => array(), // No cookies = anonymous visitor = host will cache result
937 'sslverify' => false, // Avoid SSL issues on staging/local environments
938 ));
939 }
940
941 /**
942 * Strip known tracking/analytics query parameters from a URL.
943 *
944 * CDNs treat `/page/` and `/page/?utm_source=email` as separate cache entries.
945 * Normalizing before purge/warm ensures we always target the canonical URL.
946 * Non-tracking parameters (e.g. `?tab=pricing`) are preserved.
947 *
948 * @param string $url The URL to normalize
949 * @return string The URL with tracking parameters removed
950 */
951 private static function normalize_url($url)
952 {
953 if (empty($url) || strpos($url, '?') === false) {
954 return $url;
955 }
956
957 $tracking_params = array(
958 // Google Analytics / Ads
959 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
960 'gclid', 'gbraid', 'wbraid', 'dclid',
961 // Google Analytics client IDs
962 '_ga', '_gl',
963 // Meta (Facebook / Instagram)
964 'fbclid', 'igshid',
965 // Microsoft Ads
966 'msclkid',
967 // TikTok
968 'ttclid',
969 // Twitter / X
970 'twclid',
971 // LinkedIn
972 'li_fat_id',
973 // HubSpot
974 'mc_eid', '_hsenc', '_hsmi',
975 // Yandex
976 'yclid',
977 // Zanox / Awin
978 'zanpid',
979 // Adobe Analytics
980 's_kwcid',
981 );
982
983 return remove_query_arg($tracking_params, $url);
984 }
985
986 /**
987 * Log a cache clear failure consistently
988 *
989 * @param string $plugin_name Human-readable plugin name
990 * @param \Throwable $e The caught exception or error
991 */
992 private function log_cache_error($plugin_name, $e)
993 {
994 error_log('MetaSync: Failed to clear ' . $plugin_name . ' cache - ' . $e->getMessage());
995 }
996
997 /**
998 * Check if a plugin is active
999 *
1000 * @param string $plugin_path Plugin path (folder/file.php)
1001 * @return bool
1002 */
1003 private function is_plugin_active($plugin_path)
1004 {
1005 if (!function_exists('is_plugin_active')) {
1006 include_once(ABSPATH . 'wp-admin/includes/plugin.php');
1007 }
1008 return is_plugin_active($plugin_path);
1009 }
1010
1011 /**
1012 * Get list of active cache plugins
1013 *
1014 * @return array List of active cache plugin names
1015 */
1016 public function get_active_cache_plugins()
1017 {
1018 $cache_plugins = array(
1019 'wp-rocket/wp-rocket.php' => 'WP Rocket',
1020 'litespeed-cache/litespeed-cache.php' => 'LiteSpeed Cache',
1021 'w3-total-cache/w3-total-cache.php' => 'W3 Total Cache',
1022 'wp-super-cache/wp-cache.php' => 'WP Super Cache',
1023 'wp-fastest-cache/wpFastestCache.php' => 'WP Fastest Cache',
1024 'cache-enabler/cache-enabler.php' => 'Cache Enabler',
1025 'hummingbird-performance/wp-hummingbird.php' => 'Hummingbird',
1026 'autoptimize/autoptimize.php' => 'Autoptimize',
1027 'sg-cachepress/sg-cachepress.php' => 'SG Optimizer',
1028 'comet-cache/comet-cache.php' => 'Comet Cache',
1029 'swift-performance-lite/performance.php' => 'Swift Performance Lite',
1030 'swift-performance/performance.php' => 'Swift Performance',
1031 );
1032
1033 $active = array();
1034 foreach ($cache_plugins as $plugin_path => $plugin_name) {
1035 if ($this->is_plugin_active($plugin_path)) {
1036 $active[$plugin_path] = $plugin_name;
1037 }
1038 }
1039
1040 return $active;
1041 }
1042 }
1043
1044
1045
1046