| @@ -77,10 +77,23 @@ | ||
| 77 | 77 | 'permission_callback' => array( $this, 'permissions' ), |
| 78 | 78 | ) |
| 79 | 79 | ); |
| 80 | 80 | |
| 81 | + // Force a fresh static-rewrite probe. The result is otherwise cached | |
| 82 | + // for five minutes with nothing to invalidate it, so a user who just | |
| 83 | + // fixed their nginx config had no way to confirm it. (FBS-84012) | |
| 81 | 84 | register_rest_route( |
| 82 | 85 | self::NAMESPACE_V1, |
| 86 | + '/cache/recheck-rewrite', | |
| 87 | + array( | |
| 88 | + 'methods' => 'POST', | |
| 89 | + 'callback' => array( $this, 'recheck_rewrite' ), | |
| 90 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 91 | + ) | |
| 92 | + ); | |
| 93 | + | |
| 94 | + register_rest_route( | |
| 95 | + self::NAMESPACE_V1, | |
| 83 | 96 | '/cache/benchmark', |
| 84 | 97 | array( |
| 85 | 98 | 'methods' => 'GET', |
| 86 | 99 | 'callback' => array( $this, 'benchmark' ), |
| @@ -97,10 +110,63 @@ | ||
| 97 | 110 | 'permission_callback' => array( $this, 'permissions' ), |
| 98 | 111 | ) |
| 99 | 112 | ); |
| 100 | 113 | |
| 114 | + // Drill-downs behind the four dashboard stat cards. Each is a plain | |
| 115 | + // GET so the same data reaches the CLI and MCP through | |
| 116 | + // `wp xspeed cache inventory|size|purge-log`. | |
| 101 | 117 | register_rest_route( |
| 102 | 118 | self::NAMESPACE_V1, |
| 119 | + '/cache/inventory', | |
| 120 | + array( | |
| 121 | + 'methods' => 'GET', | |
| 122 | + 'callback' => array( $this, 'cache_inventory' ), | |
| 123 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 124 | + 'args' => array( | |
| 125 | + 'limit' => array( | |
| 126 | + 'type' => 'integer', | |
| 127 | + 'default' => 50, | |
| 128 | + ), | |
| 129 | + 'offset' => array( | |
| 130 | + 'type' => 'integer', | |
| 131 | + 'default' => 0, | |
| 132 | + ), | |
| 133 | + 'fresh' => array( | |
| 134 | + 'type' => 'boolean', | |
| 135 | + 'default' => false, | |
| 136 | + ), | |
| 137 | + ), | |
| 138 | + ) | |
| 139 | + ); | |
| 140 | + | |
| 141 | + register_rest_route( | |
| 142 | + self::NAMESPACE_V1, | |
| 143 | + '/cache/size', | |
| 144 | + array( | |
| 145 | + 'methods' => 'GET', | |
| 146 | + 'callback' => array( $this, 'cache_size' ), | |
| 147 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 148 | + ) | |
| 149 | + ); | |
| 150 | + | |
| 151 | + register_rest_route( | |
| 152 | + self::NAMESPACE_V1, | |
| 153 | + '/cache/purge-log', | |
| 154 | + array( | |
| 155 | + 'methods' => 'GET', | |
| 156 | + 'callback' => array( $this, 'cache_purge_log' ), | |
| 157 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 158 | + 'args' => array( | |
| 159 | + 'limit' => array( | |
| 160 | + 'type' => 'integer', | |
| 161 | + 'default' => 25, | |
| 162 | + ), | |
| 163 | + ), | |
| 164 | + ) | |
| 165 | + ); | |
| 166 | + | |
| 167 | + register_rest_route( | |
| 168 | + self::NAMESPACE_V1, | |
| 103 | 169 | '/stats/hit-daily', |
| 104 | 170 | array( |
| 105 | 171 | 'methods' => 'GET', |
| 106 | 172 | 'callback' => array( $this, 'hit_daily' ), |
| @@ -139,8 +205,18 @@ | ||
| 139 | 205 | ); |
| 140 | 206 | |
| 141 | 207 | register_rest_route( |
| 142 | 208 | self::NAMESPACE_V1, |
| 209 | + '/activity', | |
| 210 | + array( | |
| 211 | + 'methods' => 'GET', | |
| 212 | + 'callback' => array( $this, 'activity' ), | |
| 213 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 214 | + ) | |
| 215 | + ); | |
| 216 | + | |
| 217 | + register_rest_route( | |
| 218 | + self::NAMESPACE_V1, | |
| 143 | 219 | '/modules', |
| 144 | 220 | array( |
| 145 | 221 | 'methods' => 'GET', |
| 146 | 222 | 'callback' => array( $this, 'get_modules' ), |
| @@ -213,8 +289,20 @@ | ||
| 213 | 289 | return rest_ensure_response( array( 'suggestions' => Pro_Audit::run() ) ); |
| 214 | 290 | } |
| 215 | 291 | |
| 216 | 292 | /** |
| 293 | + * Recent activity-log entries for the Overview activity strip — | |
| 294 | + * newest-first (settings changes, purges, cache toggles, …). | |
| 295 | + * | |
| 296 | + * @param \WP_REST_Request $request Unused. | |
| 297 | + * @return \WP_REST_Response | |
| 298 | + */ | |
| 299 | + public function activity( $request ) { | |
| 300 | + unset( $request ); | |
| 301 | + return rest_ensure_response( array( 'activity' => Activity_Log::entries() ) ); | |
| 302 | + } | |
| 303 | + | |
| 304 | + /** | |
| 217 | 305 | * Cache before/after benchmark — fetches home_url() twice (with + |
| 218 | 306 | * without the bypass header) and returns side-by-side timings for |
| 219 | 307 | * the dashboard widget. |
| 220 | 308 | */ |
| @@ -311,8 +399,15 @@ | ||
| 311 | 399 | if ( $opts['cache_enabled'] && $rewrite_capable ) { |
| 312 | 400 | $probe = Cache::probe_static_rewrite(); |
| 313 | 401 | $rewrite_probe = array( |
| 314 | 402 | 'active' => (bool) ( $probe['active'] ?? false ), |
| 403 | + // Both flags were previously dropped here, so the dashboard | |
| 404 | + // could not tell "proven inactive" from "no result yet" or | |
| 405 | + // "probe failed" — and rendered the configure-your-server | |
| 406 | + // banner for all three. (FBS-84012) | |
| 407 | + 'pending' => (bool) ( $probe['pending'] ?? false ), | |
| 408 | + 'inconclusive' => (bool) ( $probe['inconclusive'] ?? false ), | |
| 409 | + 'reason' => (string) ( $probe['reason'] ?? '' ), | |
| 315 | 410 | 'server_type' => $server_type, |
| 316 | 411 | 'snippet' => Cache::nginx_snippet(), |
| 317 | 412 | 'topology' => Server::rewrite_topology(), |
| 318 | 413 | 'behind_proxy' => Server::is_behind_proxy(), |
| @@ -330,8 +425,13 @@ | ||
| 330 | 425 | 'nginx_snippet' => Gzip::nginx_snippet(), |
| 331 | 426 | ), |
| 332 | 427 | 'rewrite_probe' => $rewrite_probe, |
| 333 | 428 | 'nginx_server_block' => Cache::full_nginx_server_block(), |
| 429 | + // What enabling the page cache would do to | |
| 430 | + // wp-content/advanced-cache.php. The dashboard discloses the | |
| 431 | + // replacement BEFORE the write when a leftover drop-in is | |
| 432 | + // already there; see Page_Cache_Detector::dropin_disclosure(). | |
| 433 | + 'dropin' => Page_Cache_Detector::dropin_disclosure(), | |
| 334 | 434 | // Separate Mobile Cache visibility (FBS-83145). `blocking` is |
| 335 | 435 | // true when mobile_separate is what's keeping the device-blind |
| 336 | 436 | // static fast path from installing on a rewrite-capable server; |
| 337 | 437 | // `needs_review` is true when a migration turned it on for us and |
| @@ -364,13 +464,47 @@ | ||
| 364 | 464 | return rest_ensure_response( $updated ); |
| 365 | 465 | } |
| 366 | 466 | |
| 367 | 467 | public function purge() { |
| 368 | - Cache::purge_all(); | |
| 369 | - return rest_ensure_response( array( 'stats' => Cache::get_stats() ) ); | |
| 468 | + // The same core function `wp xspeed purge` runs, so the dashboard | |
| 469 | + // button and the CLI cannot clear different sets of stores — and the | |
| 470 | + // per-store report is available here for the UI to surface a store | |
| 471 | + // that was skipped or refused rather than flashing "cache cleared". | |
| 472 | + $report = Purge_Runner::run( array( 'all' ), __( 'dashboard', 'xspeed' ) ); | |
| 473 | + return rest_ensure_response( | |
| 474 | + array( | |
| 475 | + 'stats' => Cache::get_stats(), | |
| 476 | + 'report' => $report, | |
| 477 | + ) | |
| 478 | + ); | |
| 370 | 479 | } |
| 371 | 480 | |
| 372 | 481 | /** |
| 482 | + * The "Cached Pages" drill-down: which pages are cached and how old they | |
| 483 | + * are. Paginated because a busy site's cache is thousands of entries and | |
| 484 | + * the answer to "is my cache working" doesn't need all of them at once. | |
| 485 | + */ | |
| 486 | + public function cache_inventory( \WP_REST_Request $request ) { | |
| 487 | + return rest_ensure_response( | |
| 488 | + Cache_Inventory::entries( | |
| 489 | + (int) $request->get_param( 'limit' ), | |
| 490 | + (int) $request->get_param( 'offset' ), | |
| 491 | + (bool) $request->get_param( 'fresh' ) | |
| 492 | + ) | |
| 493 | + ); | |
| 494 | + } | |
| 495 | + | |
| 496 | + /** The "Cache Size" drill-down: where the bytes actually go. */ | |
| 497 | + public function cache_size() { | |
| 498 | + return rest_ensure_response( Cache_Inventory::size_breakdown() ); | |
| 499 | + } | |
| 500 | + | |
| 501 | + /** The "Last Purge" drill-down: what cleared the cache, when, and why. */ | |
| 502 | + public function cache_purge_log( \WP_REST_Request $request ) { | |
| 503 | + return rest_ensure_response( Cache_Inventory::purge_log( (int) $request->get_param( 'limit' ) ) ); | |
| 504 | + } | |
| 505 | + | |
| 506 | + /** | |
| 373 | 507 | * Resolved branding ({name, footer_credit, hide_help_links, logo_svg}). |
| 374 | 508 | * Runs the `xspeed_branding` filter so Pro's white-label override is |
| 375 | 509 | * reflected. Consumed by the dashboard's post-save branding refresh. |
| 376 | 510 | */ |
| @@ -377,8 +511,42 @@ | ||
| 377 | 511 | public function get_branding() { |
| 378 | 512 | return rest_ensure_response( Admin::branding() ); |
| 379 | 513 | } |
| 380 | 514 | |
| 515 | + /** | |
| 516 | + * Re-run the static-rewrite probe, bypassing the cached result. | |
| 517 | + * | |
| 518 | + * Returns the same shape the dashboard bootstrap uses, so the caller can | |
| 519 | + * swap it straight into state without a second round trip. (FBS-84012) | |
| 520 | + */ | |
| 521 | + public function recheck_rewrite() { | |
| 522 | + $raw = Cache::recheck_static_rewrite(); | |
| 523 | + $server_type = Server::detect(); | |
| 524 | + | |
| 525 | + // Qualify the raw probe against known config refusals. The probe | |
| 526 | + // fetches its own file from the static tree, which succeeds even when | |
| 527 | + // no real page is served that way — so an unqualified `active` told | |
| 528 | + // clients the static path was engaged on sites where it demonstrably | |
| 529 | + // wasn't. `block_reason` is exposed so a client can act on the | |
| 530 | + // specific cause rather than re-deriving it. See | |
| 531 | + // Cache::qualify_rewrite_probe(). | |
| 532 | + $probe = Cache::qualify_rewrite_probe( $raw ); | |
| 533 | + | |
| 534 | + return rest_ensure_response( | |
| 535 | + array( | |
| 536 | + 'active' => $probe['active'], | |
| 537 | + 'pending' => (bool) ( $raw['pending'] ?? false ), | |
| 538 | + 'inconclusive' => $probe['inconclusive'], | |
| 539 | + 'reason' => $probe['reason'], | |
| 540 | + 'block_reason' => $probe['block_reason'], | |
| 541 | + 'server_type' => $server_type, | |
| 542 | + 'snippet' => Cache::nginx_snippet(), | |
| 543 | + 'topology' => Server::rewrite_topology(), | |
| 544 | + 'behind_proxy' => Server::is_behind_proxy(), | |
| 545 | + ) | |
| 546 | + ); | |
| 547 | + } | |
| 548 | + | |
| 381 | 549 | public function toggle_cache( \WP_REST_Request $request ) { |
| 382 | 550 | $params = $request->get_json_params(); |
| 383 | 551 | $enabled = isset( $params['enabled'] ) ? (bool) $params['enabled'] : false; |
| 384 | 552 | |
| @@ -386,9 +554,9 @@ | ||
| 386 | 554 | // permission_callback above already enforced current_user_can( |
| 387 | 555 | // 'manage_options' ); the REST nonce is verified by core via the |
| 388 | 556 | // X-WP-Nonce header. |
| 389 | 557 | $state = Cache::toggle( $enabled ); |
| 390 | - $updated = Settings::update( array( 'cache_enabled' => $state['enabled'] ) ); | |
| 558 | + $updated = Settings::get(); | |
| 391 | 559 | |
| 392 | 560 | // Recompute the unified nginx block AFTER cache_enabled is persisted. |
| 393 | 561 | // Cache::toggle() computes it inline, but cache_enabled isn't written |
| 394 | 562 | // until the Settings::update() above — so the block inside $state |
| @@ -398,11 +566,17 @@ | ||
| 398 | 566 | $state['nginx_server_block'] = Cache::full_nginx_server_block(); |
| 399 | 567 | |
| 400 | 568 | return rest_ensure_response( |
| 401 | 569 | array( |
| 402 | - 'enabled' => $updated['cache_enabled'], | |
| 403 | - 'stats' => Cache::get_stats(), | |
| 404 | - 'install_state' => $state, | |
| 570 | + 'enabled' => $updated['cache_enabled'], | |
| 571 | + // Surfaced at the top level so the dashboard can explain a | |
| 572 | + // refusal rather than silently snapping the toggle back: | |
| 573 | + // Cache::toggle() writes nothing when another plugin owns the | |
| 574 | + // drop-in or WP_CACHE is in a shape we must not rewrite. | |
| 575 | + 'blocked' => ! empty( $state['blocked'] ), | |
| 576 | + 'blocked_reason' => $state['blocked_reason'] ?? null, | |
| 577 | + 'stats' => Cache::get_stats(), | |
| 578 | + 'install_state' => $state, | |
| 405 | 579 | ) |
| 406 | 580 | ); |
| 407 | 581 | } |
| 408 | 582 | } |