class-avcf-abilities-base.php
3 weeks ago
class-avcf-abilities-block-navigation.php
3 weeks ago
class-avcf-abilities-cache.php
3 weeks ago
class-avcf-abilities-content.php
3 weeks ago
class-avcf-abilities-core.php
3 weeks ago
class-avcf-abilities-global-styles.php
3 weeks ago
class-avcf-abilities-gutenberg.php
3 weeks ago
class-avcf-abilities-media.php
3 weeks ago
class-avcf-abilities-metadata.php
3 weeks ago
class-avcf-abilities-navigation.php
3 weeks ago
class-avcf-abilities-patterns.php
3 weeks ago
class-avcf-abilities-plugins.php
3 weeks ago
class-avcf-abilities-settings.php
3 weeks ago
class-avcf-abilities-taxonomies.php
3 weeks ago
class-avcf-abilities-templates.php
3 weeks ago
class-avcf-abilities-theme-files.php
3 weeks ago
class-avcf-abilities-themes.php
3 weeks ago
class-avcf-abilities-users.php
3 weeks ago
class-avcf-abilities-cache.php
465 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Cache / CDN purge MCP abilities. |
| 4 | * |
| 5 | * WordPress core has no universal cache- or CDN-purge API: object caches, page |
| 6 | * cache plugins, and CDN/host edge layers each expose their own mechanism. This |
| 7 | * category provides a GENERIC, best-effort purge that dispatches to every cache |
| 8 | * layer it can detect on the site and honestly reports which ones actually |
| 9 | * fired — rather than pretending a single call cleared everything. |
| 10 | * |
| 11 | * Detection is guarded (function_exists / class_exists / has_action) so nothing |
| 12 | * here hard-depends on any cache plugin being installed. Every provider call is |
| 13 | * wrapped in try/catch so one broken provider never aborts the rest. |
| 14 | * |
| 15 | * Extensibility: after the built-in providers run, the following actions fire so |
| 16 | * site owners can purge custom or host-specific layers: |
| 17 | * do_action( 'atarim_purge_all_caches' ) |
| 18 | * do_action( 'atarim_purge_url_cache', $url ) |
| 19 | * |
| 20 | * Limitation: true CDN edge purge WITHOUT an installed integration plugin |
| 21 | * (e.g. raw Cloudflare with no Cloudflare plugin) needs per-CDN API credentials |
| 22 | * and is out of scope. This covers everything reachable through hooks/functions |
| 23 | * already loaded on the site. |
| 24 | * |
| 25 | * Exposed abilities: |
| 26 | * atarim/purge-all-caches Flush object cache + every detected page/CDN cache. |
| 27 | * atarim/purge-url-cache Purge a single URL where the provider supports it. |
| 28 | * atarim/get-cache-status Report which cache layers are detected (read-only). |
| 29 | * |
| 30 | * @package atarim-visual-collaboration |
| 31 | */ |
| 32 | |
| 33 | if ( ! defined('ABSPATH') ) { |
| 34 | exit; |
| 35 | } |
| 36 | |
| 37 | class AVCF_Abilities_Cache extends AVCF_Abilities_Base { |
| 38 | |
| 39 | /** |
| 40 | * Register all cache abilities. |
| 41 | * Called from AVCF_MCP::avcf_mcp_register_abilities() on wp_abilities_api_init. |
| 42 | */ |
| 43 | public function register() { |
| 44 | |
| 45 | // ---- purge-all-caches ---- |
| 46 | wp_register_ability( 'atarim/purge-all-caches', [ |
| 47 | 'label' => 'Purge All Caches', |
| 48 | 'description' => 'Best-effort purge of every cache layer detected on the site: the WordPress object cache plus popular page-cache and CDN/host plugins (WP Rocket, W3 Total Cache, WP Super Cache, LiteSpeed, WP Fastest Cache, Cache Enabler, Autoptimize, SG Optimizer, Nginx Helper, WP Engine, Kinsta, Cloudways Breeze). Each provider is detected before it is called, and the response reports exactly which ones flushed, which were absent, and which errored. Note: purging a CDN edge with NO integration plugin installed (e.g. raw Cloudflare) needs API credentials and is NOT handled here. Also fires the atarim_purge_all_caches action so custom providers can hook in.', |
| 49 | 'category' => 'atarim', |
| 50 | 'input_schema' => [ |
| 51 | 'type' => 'object', |
| 52 | 'properties' => new \stdClass(), |
| 53 | 'additionalProperties' => false, |
| 54 | ], |
| 55 | 'output_schema' => [ |
| 56 | 'type' => 'object', |
| 57 | 'properties' => [ |
| 58 | 'success' => [ 'type' => 'boolean' ], |
| 59 | 'purged' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], |
| 60 | 'skipped' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], |
| 61 | 'errors' => [ |
| 62 | 'type' => 'array', |
| 63 | 'items' => [ |
| 64 | 'type' => 'object', |
| 65 | 'properties' => [ |
| 66 | 'provider' => [ 'type' => 'string' ], |
| 67 | 'message' => [ 'type' => 'string' ], |
| 68 | ], |
| 69 | ], |
| 70 | ], |
| 71 | 'message' => [ 'type' => 'string' ], |
| 72 | ], |
| 73 | 'required' => [ 'success', 'purged', 'skipped', 'errors', 'message' ], |
| 74 | ], |
| 75 | 'execute_callback' => function( $input = [] ) { |
| 76 | $purged = []; |
| 77 | $skipped = []; |
| 78 | $errors = []; |
| 79 | |
| 80 | foreach ( $this->avcf_cache_providers() as $provider ) { |
| 81 | if ( ! $this->avcf_provider_detected( $provider ) ) { |
| 82 | $skipped[] = $provider['label']; |
| 83 | continue; |
| 84 | } |
| 85 | try { |
| 86 | call_user_func( $provider['purge_all'] ); |
| 87 | $purged[] = $provider['label']; |
| 88 | } catch ( \Throwable $e ) { |
| 89 | $errors[] = [ 'provider' => $provider['label'], 'message' => $e->getMessage() ]; |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | // Extensibility hook for custom / host-specific layers. |
| 94 | do_action( 'atarim_purge_all_caches' ); |
| 95 | |
| 96 | $success = empty( $errors ); |
| 97 | $message = sprintf( |
| 98 | 'Purged %d cache layer(s): %s. Skipped %d (not present). %d error(s).', |
| 99 | count( $purged ), |
| 100 | $purged ? implode( ', ', $purged ) : 'none', |
| 101 | count( $skipped ), |
| 102 | count( $errors ) |
| 103 | ); |
| 104 | |
| 105 | return [ |
| 106 | 'success' => $success, |
| 107 | 'purged' => $purged, |
| 108 | 'skipped' => $skipped, |
| 109 | 'errors' => $errors, |
| 110 | 'message' => $message, |
| 111 | ]; |
| 112 | }, |
| 113 | 'permission_callback' => function() { |
| 114 | return current_user_can( 'manage_options' ); |
| 115 | }, |
| 116 | 'meta' => [ |
| 117 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 118 | 'annotations' => [ 'readonly' => false, 'destructive' => false, 'idempotent' => true ], |
| 119 | ], |
| 120 | ] ); |
| 121 | |
| 122 | // ---- purge-url-cache ---- |
| 123 | wp_register_ability( 'atarim/purge-url-cache', [ |
| 124 | 'label' => 'Purge URL Cache', |
| 125 | 'description' => 'Best-effort purge of the cached copy of a single URL. Only providers that support per-URL purging are invoked (WP Rocket, W3 Total Cache, LiteSpeed, Cache Enabler); others are reported as unsupported. The object cache is not touched. Also fires the atarim_purge_url_cache action with the URL so custom providers can hook in. For a full site-wide flush use atarim/purge-all-caches instead.', |
| 126 | 'category' => 'atarim', |
| 127 | 'input_schema' => [ |
| 128 | 'type' => 'object', |
| 129 | 'properties' => [ |
| 130 | 'url' => [ |
| 131 | 'type' => 'string', |
| 132 | 'description' => 'Absolute URL whose cached copy should be purged, e.g. https://example.com/about/.', |
| 133 | ], |
| 134 | ], |
| 135 | 'required' => [ 'url' ], |
| 136 | 'additionalProperties' => false, |
| 137 | ], |
| 138 | 'output_schema' => [ |
| 139 | 'type' => 'object', |
| 140 | 'properties' => [ |
| 141 | 'success' => [ 'type' => 'boolean' ], |
| 142 | 'url' => [ 'type' => 'string' ], |
| 143 | 'purged' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], |
| 144 | 'unsupported' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], |
| 145 | 'errors' => [ |
| 146 | 'type' => 'array', |
| 147 | 'items' => [ |
| 148 | 'type' => 'object', |
| 149 | 'properties' => [ |
| 150 | 'provider' => [ 'type' => 'string' ], |
| 151 | 'message' => [ 'type' => 'string' ], |
| 152 | ], |
| 153 | ], |
| 154 | ], |
| 155 | 'message' => [ 'type' => 'string' ], |
| 156 | ], |
| 157 | 'required' => [ 'success', 'url', 'purged', 'unsupported', 'errors', 'message' ], |
| 158 | ], |
| 159 | 'execute_callback' => function( $input = [] ) { |
| 160 | $url = isset( $input['url'] ) ? esc_url_raw( trim( (string) $input['url'] ) ) : ''; |
| 161 | if ( $url === '' ) { |
| 162 | return [ |
| 163 | 'success' => false, |
| 164 | 'url' => '', |
| 165 | 'purged' => [], |
| 166 | 'unsupported' => [], |
| 167 | 'errors' => [], |
| 168 | 'message' => 'url must be a non-empty, valid absolute URL.', |
| 169 | ]; |
| 170 | } |
| 171 | |
| 172 | $purged = []; |
| 173 | $unsupported = []; |
| 174 | $errors = []; |
| 175 | |
| 176 | foreach ( $this->avcf_cache_providers() as $provider ) { |
| 177 | if ( empty( $provider['purge_url'] ) ) { |
| 178 | continue; // Provider has no per-URL mechanism at all — not reported. |
| 179 | } |
| 180 | if ( ! $this->avcf_provider_detected( $provider ) ) { |
| 181 | continue; // Not installed — silent; only report installed-but-unsupported below. |
| 182 | } |
| 183 | try { |
| 184 | call_user_func( $provider['purge_url'], $url ); |
| 185 | $purged[] = $provider['label']; |
| 186 | } catch ( \Throwable $e ) { |
| 187 | $errors[] = [ 'provider' => $provider['label'], 'message' => $e->getMessage() ]; |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | // Report detected page/CDN providers that can only flush site-wide. |
| 192 | // The object cache is intentionally excluded — it is not a per-URL layer. |
| 193 | foreach ( $this->avcf_cache_providers() as $provider ) { |
| 194 | if ( $provider['key'] === 'object_cache' || ! empty( $provider['purge_url'] ) ) { |
| 195 | continue; |
| 196 | } |
| 197 | if ( $this->avcf_provider_detected( $provider ) ) { |
| 198 | $unsupported[] = $provider['label']; |
| 199 | } |
| 200 | } |
| 201 | |
| 202 | // Extensibility hook for custom / host-specific layers. |
| 203 | do_action( 'atarim_purge_url_cache', $url ); |
| 204 | |
| 205 | $success = empty( $errors ); |
| 206 | $message = sprintf( |
| 207 | 'Purged URL from %d provider(s): %s. %d detected provider(s) support site-wide flush only. %d error(s).', |
| 208 | count( $purged ), |
| 209 | $purged ? implode( ', ', $purged ) : 'none', |
| 210 | count( $unsupported ), |
| 211 | count( $errors ) |
| 212 | ); |
| 213 | |
| 214 | return [ |
| 215 | 'success' => $success, |
| 216 | 'url' => $url, |
| 217 | 'purged' => $purged, |
| 218 | 'unsupported' => $unsupported, |
| 219 | 'errors' => $errors, |
| 220 | 'message' => $message, |
| 221 | ]; |
| 222 | }, |
| 223 | 'permission_callback' => function() { |
| 224 | return current_user_can( 'manage_options' ); |
| 225 | }, |
| 226 | 'meta' => [ |
| 227 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 228 | 'annotations' => [ 'readonly' => false, 'destructive' => false, 'idempotent' => true ], |
| 229 | ], |
| 230 | ] ); |
| 231 | |
| 232 | // ---- get-cache-status ---- |
| 233 | wp_register_ability( 'atarim/get-cache-status', [ |
| 234 | 'label' => 'Get Cache Status', |
| 235 | 'description' => 'Reports which cache layers are currently detected on the site (object cache, page-cache plugins, CDN/host integrations) without purging anything. Use this before purging to understand what a purge-all-caches call would affect. Also reports whether a persistent object cache backend is active.', |
| 236 | 'category' => 'atarim', |
| 237 | 'input_schema' => [ |
| 238 | 'type' => 'object', |
| 239 | 'properties' => new \stdClass(), |
| 240 | 'additionalProperties' => false, |
| 241 | ], |
| 242 | 'output_schema' => [ |
| 243 | 'type' => 'object', |
| 244 | 'properties' => [ |
| 245 | 'object_cache_persistent' => [ 'type' => 'boolean' ], |
| 246 | 'detected' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], |
| 247 | 'not_detected' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], |
| 248 | 'supports_url_purge' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], |
| 249 | 'message' => [ 'type' => 'string' ], |
| 250 | ], |
| 251 | 'required' => [ 'detected', 'not_detected', 'message' ], |
| 252 | ], |
| 253 | 'execute_callback' => function( $input = [] ) { |
| 254 | $detected = []; |
| 255 | $not_detected = []; |
| 256 | $url_purge = []; |
| 257 | |
| 258 | foreach ( $this->avcf_cache_providers() as $provider ) { |
| 259 | if ( $provider['key'] === 'object_cache' ) { |
| 260 | continue; // Reported separately below. |
| 261 | } |
| 262 | if ( $this->avcf_provider_detected( $provider ) ) { |
| 263 | $detected[] = $provider['label']; |
| 264 | if ( ! empty( $provider['purge_url'] ) ) { |
| 265 | $url_purge[] = $provider['label']; |
| 266 | } |
| 267 | } else { |
| 268 | $not_detected[] = $provider['label']; |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | return [ |
| 273 | 'object_cache_persistent' => (bool) wp_using_ext_object_cache(), |
| 274 | 'detected' => $detected, |
| 275 | 'not_detected' => $not_detected, |
| 276 | 'supports_url_purge' => $url_purge, |
| 277 | 'message' => sprintf( |
| 278 | '%d cache layer(s) detected: %s.', |
| 279 | count( $detected ), |
| 280 | $detected ? implode( ', ', $detected ) : 'none (object cache only)' |
| 281 | ), |
| 282 | ]; |
| 283 | }, |
| 284 | 'permission_callback' => function() { |
| 285 | return current_user_can( 'manage_options' ); |
| 286 | }, |
| 287 | 'meta' => [ |
| 288 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 289 | 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ], |
| 290 | ], |
| 291 | ] ); |
| 292 | } |
| 293 | |
| 294 | /** |
| 295 | * Whether a provider is present on this site. |
| 296 | * |
| 297 | * @param array $provider |
| 298 | * @return bool |
| 299 | */ |
| 300 | protected function avcf_provider_detected( $provider ) { |
| 301 | try { |
| 302 | return (bool) call_user_func( $provider['detect'] ); |
| 303 | } catch ( \Throwable $e ) { |
| 304 | return false; |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | /** |
| 309 | * Best-effort cache provider dispatch table. |
| 310 | * |
| 311 | * Each entry: |
| 312 | * key string stable identifier |
| 313 | * label string human-readable name (used in reports) |
| 314 | * detect callable ():bool — is this provider present? |
| 315 | * purge_all callable ():void — flush everything for this provider |
| 316 | * purge_url callable|null (string):void — purge one URL, or null if unsupported |
| 317 | * |
| 318 | * @return array<int,array> |
| 319 | */ |
| 320 | protected function avcf_cache_providers() { |
| 321 | return [ |
| 322 | // --- Object cache (WordPress core; always available) --- |
| 323 | [ |
| 324 | 'key' => 'object_cache', |
| 325 | 'label' => 'WordPress object cache', |
| 326 | 'detect' => function() { return function_exists( 'wp_cache_flush' ); }, |
| 327 | 'purge_all' => function() { wp_cache_flush(); }, |
| 328 | 'purge_url' => null, |
| 329 | ], |
| 330 | |
| 331 | // --- Page cache plugins --- |
| 332 | [ |
| 333 | 'key' => 'wp_rocket', |
| 334 | 'label' => 'WP Rocket', |
| 335 | 'detect' => function() { return function_exists( 'rocket_clean_domain' ); }, |
| 336 | 'purge_all' => function() { rocket_clean_domain(); }, |
| 337 | 'purge_url' => function( $url ) { |
| 338 | if ( function_exists( 'rocket_clean_files' ) ) { |
| 339 | rocket_clean_files( $url ); |
| 340 | } |
| 341 | }, |
| 342 | ], |
| 343 | [ |
| 344 | 'key' => 'w3tc', |
| 345 | 'label' => 'W3 Total Cache', |
| 346 | 'detect' => function() { return function_exists( 'w3tc_flush_all' ); }, |
| 347 | 'purge_all' => function() { w3tc_flush_all(); }, |
| 348 | 'purge_url' => function( $url ) { |
| 349 | if ( function_exists( 'w3tc_flush_url' ) ) { |
| 350 | w3tc_flush_url( $url ); |
| 351 | } |
| 352 | }, |
| 353 | ], |
| 354 | [ |
| 355 | 'key' => 'wp_super_cache', |
| 356 | 'label' => 'WP Super Cache', |
| 357 | 'detect' => function() { return function_exists( 'wp_cache_clear_cache' ); }, |
| 358 | 'purge_all' => function() { wp_cache_clear_cache(); }, |
| 359 | 'purge_url' => null, |
| 360 | ], |
| 361 | [ |
| 362 | 'key' => 'litespeed', |
| 363 | 'label' => 'LiteSpeed Cache', |
| 364 | 'detect' => function() { return defined( 'LSCWP_V' ) || has_action( 'litespeed_purge_all' ); }, |
| 365 | 'purge_all' => function() { do_action( 'litespeed_purge_all' ); }, |
| 366 | 'purge_url' => function( $url ) { do_action( 'litespeed_purge_url', $url ); }, |
| 367 | ], |
| 368 | [ |
| 369 | 'key' => 'wp_fastest_cache', |
| 370 | 'label' => 'WP Fastest Cache', |
| 371 | 'detect' => function() { |
| 372 | global $wp_fastest_cache; |
| 373 | return is_object( $wp_fastest_cache ) && method_exists( $wp_fastest_cache, 'deleteCache' ); |
| 374 | }, |
| 375 | 'purge_all' => function() { |
| 376 | global $wp_fastest_cache; |
| 377 | $wp_fastest_cache->deleteCache( true ); |
| 378 | }, |
| 379 | 'purge_url' => null, |
| 380 | ], |
| 381 | [ |
| 382 | 'key' => 'cache_enabler', |
| 383 | 'label' => 'Cache Enabler', |
| 384 | 'detect' => function() { |
| 385 | return class_exists( 'Cache_Enabler' ) && method_exists( 'Cache_Enabler', 'clear_complete_cache' ); |
| 386 | }, |
| 387 | 'purge_all' => function() { Cache_Enabler::clear_complete_cache(); }, |
| 388 | 'purge_url' => function( $url ) { |
| 389 | if ( method_exists( 'Cache_Enabler', 'clear_page_cache_by_url' ) ) { |
| 390 | Cache_Enabler::clear_page_cache_by_url( $url ); |
| 391 | } |
| 392 | }, |
| 393 | ], |
| 394 | [ |
| 395 | 'key' => 'autoptimize', |
| 396 | 'label' => 'Autoptimize', |
| 397 | 'detect' => function() { |
| 398 | return class_exists( 'autoptimizeCache' ) && method_exists( 'autoptimizeCache', 'clearall' ); |
| 399 | }, |
| 400 | 'purge_all' => function() { autoptimizeCache::clearall(); }, |
| 401 | 'purge_url' => null, |
| 402 | ], |
| 403 | |
| 404 | // --- CDN / host edge integrations --- |
| 405 | [ |
| 406 | 'key' => 'sg_optimizer', |
| 407 | 'label' => 'SG Optimizer (SiteGround)', |
| 408 | 'detect' => function() { |
| 409 | return function_exists( 'sg_cachepress_purge_cache' ) || has_action( 'siteground_optimizer_flush_cache' ); |
| 410 | }, |
| 411 | 'purge_all' => function() { |
| 412 | if ( function_exists( 'sg_cachepress_purge_cache' ) ) { |
| 413 | sg_cachepress_purge_cache(); |
| 414 | } else { |
| 415 | do_action( 'siteground_optimizer_flush_cache' ); |
| 416 | } |
| 417 | }, |
| 418 | 'purge_url' => null, |
| 419 | ], |
| 420 | [ |
| 421 | 'key' => 'nginx_helper', |
| 422 | 'label' => 'Nginx Helper', |
| 423 | 'detect' => function() { return has_action( 'rt_nginx_helper_purge_all' ); }, |
| 424 | 'purge_all' => function() { do_action( 'rt_nginx_helper_purge_all' ); }, |
| 425 | 'purge_url' => null, |
| 426 | ], |
| 427 | [ |
| 428 | 'key' => 'wp_engine', |
| 429 | 'label' => 'WP Engine', |
| 430 | 'detect' => function() { |
| 431 | return class_exists( 'WpeCommon' ) && method_exists( 'WpeCommon', 'purge_varnish_cache' ); |
| 432 | }, |
| 433 | 'purge_all' => function() { WpeCommon::purge_varnish_cache(); }, |
| 434 | 'purge_url' => null, |
| 435 | ], |
| 436 | [ |
| 437 | 'key' => 'kinsta', |
| 438 | 'label' => 'Kinsta Cache', |
| 439 | 'detect' => function() { return has_action( 'kinsta_cache_purge_all' ) || function_exists( 'kinsta_cache_purge' ); }, |
| 440 | 'purge_all' => function() { |
| 441 | if ( has_action( 'kinsta_cache_purge_all' ) ) { |
| 442 | do_action( 'kinsta_cache_purge_all' ); |
| 443 | } elseif ( function_exists( 'kinsta_cache_purge' ) ) { |
| 444 | kinsta_cache_purge(); |
| 445 | } |
| 446 | }, |
| 447 | 'purge_url' => null, |
| 448 | ], |
| 449 | [ |
| 450 | 'key' => 'breeze', |
| 451 | 'label' => 'Breeze (Cloudways)', |
| 452 | 'detect' => function() { return class_exists( 'Breeze_PurgeCache' ) || has_action( 'breeze_clear_all_cache' ); }, |
| 453 | 'purge_all' => function() { |
| 454 | if ( class_exists( 'Breeze_PurgeCache' ) && method_exists( 'Breeze_PurgeCache', 'breeze_cache_flush' ) ) { |
| 455 | Breeze_PurgeCache::breeze_cache_flush(); |
| 456 | } else { |
| 457 | do_action( 'breeze_clear_all_cache' ); |
| 458 | } |
| 459 | }, |
| 460 | 'purge_url' => null, |
| 461 | ], |
| 462 | ]; |
| 463 | } |
| 464 | } |
| 465 |