| @@ -90,10 +90,29 @@ | ||
| 90 | 90 | 'permission_callback' => array( $this, 'permissions' ), |
| 91 | 91 | ) |
| 92 | 92 | ); |
| 93 | 93 | |
| 94 | + // The server-side mirror of "I pasted the block". Only meaningful on a | |
| 95 | + // host where the probe cannot read the rules back — see | |
| 96 | + // nginx_copied_hash(). | |
| 94 | 97 | register_rest_route( |
| 95 | 98 | self::NAMESPACE_V1, |
| 99 | + '/cache/nginx-copied-hash', | |
| 100 | + array( | |
| 101 | + 'methods' => 'POST', | |
| 102 | + 'callback' => array( $this, 'nginx_copied_hash' ), | |
| 103 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 104 | + 'args' => array( | |
| 105 | + 'hash' => array( | |
| 106 | + 'type' => 'string', | |
| 107 | + 'required' => true, | |
| 108 | + ), | |
| 109 | + ), | |
| 110 | + ) | |
| 111 | + ); | |
| 112 | + | |
| 113 | + register_rest_route( | |
| 114 | + self::NAMESPACE_V1, | |
| 96 | 115 | '/cache/benchmark', |
| 97 | 116 | array( |
| 98 | 117 | 'methods' => 'GET', |
| 99 | 118 | 'callback' => array( $this, 'benchmark' ), |
| @@ -180,8 +199,17 @@ | ||
| 180 | 199 | array( |
| 181 | 200 | 'methods' => 'GET', |
| 182 | 201 | 'callback' => array( $this, 'recommendations' ), |
| 183 | 202 | 'permission_callback' => array( $this, 'permissions' ), |
| 203 | + 'args' => array( | |
| 204 | + // `contributed` adds the entries other plugins hand in | |
| 205 | + // through `xspeed_recommendations`. Only the Overview | |
| 206 | + // card asks for them; see all_with_contributed(). | |
| 207 | + 'include' => array( | |
| 208 | + 'type' => 'string', | |
| 209 | + 'enum' => array( '', 'contributed' ), | |
| 210 | + ), | |
| 211 | + ), | |
| 184 | 212 | ) |
| 185 | 213 | ); |
| 186 | 214 | |
| 187 | 215 | register_rest_route( |
| @@ -340,10 +368,12 @@ | ||
| 340 | 368 | } |
| 341 | 369 | |
| 342 | 370 | /** Ranked "next best action" recommendations (issue #48). */ |
| 343 | 371 | public function recommendations( $request ) { |
| 344 | - unset( $request ); | |
| 345 | - return rest_ensure_response( array( 'recommendations' => Recommendations::all() ) ); | |
| 372 | + $recs = 'contributed' === (string) $request->get_param( 'include' ) | |
| 373 | + ? Recommendations::all_with_contributed() | |
| 374 | + : Recommendations::all(); | |
| 375 | + return rest_ensure_response( array( 'recommendations' => $recs ) ); | |
| 346 | 376 | } |
| 347 | 377 | |
| 348 | 378 | /** One-click apply of a recommendation's settings fix. */ |
| 349 | 379 | public function recommendations_apply( $request ) { |
| @@ -410,8 +440,14 @@ | ||
| 410 | 440 | 'server_type' => $server_type, |
| 411 | 441 | 'snippet' => Cache::nginx_snippet(), |
| 412 | 442 | 'topology' => Server::rewrite_topology(), |
| 413 | 443 | 'behind_proxy' => Server::is_behind_proxy(), |
| 444 | + // Whether the rules the web server is running are the rules | |
| 445 | + // these settings generate — `current`, `stale`, `absent` or | |
| 446 | + // `unknown`, with the hashes both sides compared. Nothing | |
| 447 | + // else can answer that: the nginx block lives in a server | |
| 448 | + // config WordPress cannot read. See Cache::rules_state(). | |
| 449 | + 'rules' => Cache::rules_state( $probe ), | |
| 414 | 450 | ); |
| 415 | 451 | } |
| 416 | 452 | |
| 417 | 453 | return rest_ensure_response( |
| @@ -425,8 +461,13 @@ | ||
| 425 | 461 | 'nginx_snippet' => Gzip::nginx_snippet(), |
| 426 | 462 | ), |
| 427 | 463 | 'rewrite_probe' => $rewrite_probe, |
| 428 | 464 | 'nginx_server_block' => Cache::full_nginx_server_block(), |
| 465 | + // What enabling the page cache would do to | |
| 466 | + // wp-content/advanced-cache.php. The dashboard discloses the | |
| 467 | + // replacement BEFORE the write when a leftover drop-in is | |
| 468 | + // already there; see Page_Cache_Detector::dropin_disclosure(). | |
| 469 | + 'dropin' => Page_Cache_Detector::dropin_disclosure(), | |
| 429 | 470 | // Separate Mobile Cache visibility (FBS-83145). `blocking` is |
| 430 | 471 | // true when mobile_separate is what's keeping the device-blind |
| 431 | 472 | // static fast path from installing on a rewrite-capable server; |
| 432 | 473 | // `needs_review` is true when a migration turned it on for us and |
| @@ -433,9 +474,16 @@ | ||
| 433 | 474 | // the user hasn't confirmed they actually need it. The dashboard |
| 434 | 475 | // renders a callout (+ "Check now" equality probe) from these. |
| 435 | 476 | 'mobile_separate' => array( |
| 436 | 477 | 'enabled' => ! empty( Settings::get()['cache_enabled'] ) ? (bool) ( Settings_Manager::get( 'cache' )['mobile_separate'] ?? false ) : false, |
| 437 | - 'blocking' => $rewrite_capable && 'mobile_separate' === Cache::static_rewrite_block_reason(), | |
| 478 | + // Gated to servers that HAVE a static fast path — see the | |
| 479 | + // matching comment in Admin::bootstrap_payload(): on IIS / | |
| 480 | + // unknown, block_reason still falls through to | |
| 481 | + // mobile_separate and reporting it as "blocking" would nag | |
| 482 | + // about a rewrite that does not exist there (#108). | |
| 483 | + // LiteSpeed joined the capable set with the opt-in (#509). | |
| 484 | + 'blocking' => ( $rewrite_capable || Server::LITESPEED === $server_type ) | |
| 485 | + && 'mobile_separate' === Cache::static_rewrite_block_reason(), | |
| 438 | 486 | 'needs_review' => Cache::mobile_separate_needs_review(), |
| 439 | 487 | ), |
| 440 | 488 | ) |
| 441 | 489 | ); |
| @@ -459,13 +507,19 @@ | ||
| 459 | 507 | return rest_ensure_response( $updated ); |
| 460 | 508 | } |
| 461 | 509 | |
| 462 | 510 | public function purge() { |
| 463 | - // purge_type( 'all' ) rather than purge_all() so the dashboard button | |
| 464 | - // behaves identically to the admin-bar "Purge All" — including | |
| 465 | - // clearing third-party render caches (Render_Caches). | |
| 466 | - Cache::purge_type( 'all', __( 'dashboard', 'xspeed' ) ); | |
| 467 | - return rest_ensure_response( array( 'stats' => Cache::get_stats() ) ); | |
| 511 | + // The same core function `wp xspeed purge` runs, so the dashboard | |
| 512 | + // button and the CLI cannot clear different sets of stores — and the | |
| 513 | + // per-store report is available here for the UI to surface a store | |
| 514 | + // that was skipped or refused rather than flashing "cache cleared". | |
| 515 | + $report = Purge_Runner::run( array( 'all' ), __( 'dashboard', 'xspeed' ) ); | |
| 516 | + return rest_ensure_response( | |
| 517 | + array( | |
| 518 | + 'stats' => Cache::get_stats(), | |
| 519 | + 'report' => $report, | |
| 520 | + ) | |
| 521 | + ); | |
| 468 | 522 | } |
| 469 | 523 | |
| 470 | 524 | /** |
| 471 | 525 | * The "Cached Pages" drill-down: which pages are cached and how old they |
| @@ -530,10 +584,45 @@ | ||
| 530 | 584 | 'server_type' => $server_type, |
| 531 | 585 | 'snippet' => Cache::nginx_snippet(), |
| 532 | 586 | 'topology' => Server::rewrite_topology(), |
| 533 | 587 | 'behind_proxy' => Server::is_behind_proxy(), |
| 588 | + // The whole reason to re-run the probe is usually that the | |
| 589 | + // user just pasted the block, so this is where they most need | |
| 590 | + // to be told whether the installed rules are the current ones. | |
| 591 | + // It was only ever on /status before, which the CLI and MCP | |
| 592 | + // recheck paths never call. See Cache::rules_state(). | |
| 593 | + 'rules' => $probe['rules'], | |
| 534 | 594 | ) |
| 535 | 595 | ); |
| 596 | + } | |
| 597 | + | |
| 598 | + /** | |
| 599 | + * Remember that this admin copied the current rules block. | |
| 600 | + * | |
| 601 | + * The mirror of the panel's own localStorage note, for the one case that | |
| 602 | + * note cannot cover: a host where the probe returns `unknown` — blocked | |
| 603 | + * loopback, or a CDN answering it — and a second admin, or the same admin | |
| 604 | + * on another machine, is otherwise told to paste a block that is already | |
| 605 | + * installed. Stored per user because it is a claim a person made. | |
| 606 | + * | |
| 607 | + * The hash is the rules marker, and a value that is not one is refused | |
| 608 | + * rather than stored: the mirror is only useful while it holds something | |
| 609 | + * rules_marker_expected() could also produce. | |
| 610 | + */ | |
| 611 | + public function nginx_copied_hash( \WP_REST_Request $request ) { | |
| 612 | + $params = (array) $request->get_json_params(); | |
| 613 | + $hash = isset( $params['hash'] ) ? (string) $params['hash'] : (string) $request->get_param( 'hash' ); | |
| 614 | + | |
| 615 | + $stored = Cache::remember_rules_copied( $hash ); | |
| 616 | + if ( null === $stored ) { | |
| 617 | + return new \WP_Error( | |
| 618 | + 'xspeed_invalid_rules_hash', | |
| 619 | + __( 'That is not a rules marker this site could have generated.', 'xspeed' ), | |
| 620 | + array( 'status' => 400 ) | |
| 621 | + ); | |
| 622 | + } | |
| 623 | + | |
| 624 | + return rest_ensure_response( array( 'copied' => $stored ) ); | |
| 536 | 625 | } |
| 537 | 626 | |
| 538 | 627 | public function toggle_cache( \WP_REST_Request $request ) { |
| 539 | 628 | $params = $request->get_json_params(); |