PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.0
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.0
1.4.1 1.4.0 1.3.7 1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 All 35 releases
← All changes | includes/class-rest-api.php +122 -14 1.1.2 → 1.4.0 View file →
@@ -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,10 +507,19 @@
459 507 return rest_ensure_response( $updated );
460 508 }
461 509
462 510 public function purge() {
463 - Cache::purge_all( __( 'dashboard', 'xspeed' ) );
464 - 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 + );
465 522 }
466 523
467 524 /**
468 525 * The "Cached Pages" drill-down: which pages are cached and how old they
@@ -504,25 +561,70 @@
504 561 * Returns the same shape the dashboard bootstrap uses, so the caller can
505 562 * swap it straight into state without a second round trip. (FBS-84012)
506 563 */
507 564 public function recheck_rewrite() {
508 - $probe = Cache::recheck_static_rewrite();
565 + $raw = Cache::recheck_static_rewrite();
509 566 $server_type = Server::detect();
510 567
568 + // Qualify the raw probe against known config refusals. The probe
569 + // fetches its own file from the static tree, which succeeds even when
570 + // no real page is served that way — so an unqualified `active` told
571 + // clients the static path was engaged on sites where it demonstrably
572 + // wasn't. `block_reason` is exposed so a client can act on the
573 + // specific cause rather than re-deriving it. See
574 + // Cache::qualify_rewrite_probe().
575 + $probe = Cache::qualify_rewrite_probe( $raw );
576 +
511 577 return rest_ensure_response(
512 578 array(
513 - 'active' => (bool) ( $probe['active'] ?? false ),
514 - 'pending' => (bool) ( $probe['pending'] ?? false ),
515 - 'inconclusive' => (bool) ( $probe['inconclusive'] ?? false ),
516 - 'reason' => (string) ( $probe['reason'] ?? '' ),
579 + 'active' => $probe['active'],
580 + 'pending' => (bool) ( $raw['pending'] ?? false ),
581 + 'inconclusive' => $probe['inconclusive'],
582 + 'reason' => $probe['reason'],
583 + 'block_reason' => $probe['block_reason'],
517 584 'server_type' => $server_type,
518 585 'snippet' => Cache::nginx_snippet(),
519 586 'topology' => Server::rewrite_topology(),
520 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'],
521 594 )
522 595 );
523 596 }
524 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 ) );
625 + }
626 +
525 627 public function toggle_cache( \WP_REST_Request $request ) {
526 628 $params = $request->get_json_params();
527 629 $enabled = isset( $params['enabled'] ) ? (bool) $params['enabled'] : false;
528 630
@@ -530,9 +632,9 @@
530 632 // permission_callback above already enforced current_user_can(
531 633 // 'manage_options' ); the REST nonce is verified by core via the
532 634 // X-WP-Nonce header.
533 635 $state = Cache::toggle( $enabled );
534 - $updated = Settings::update( array( 'cache_enabled' => $state['enabled'] ) );
636 + $updated = Settings::get();
535 637
536 638 // Recompute the unified nginx block AFTER cache_enabled is persisted.
537 639 // Cache::toggle() computes it inline, but cache_enabled isn't written
538 640 // until the Settings::update() above — so the block inside $state
@@ -542,11 +644,17 @@
542 644 $state['nginx_server_block'] = Cache::full_nginx_server_block();
543 645
544 646 return rest_ensure_response(
545 647 array(
546 - 'enabled' => $updated['cache_enabled'],
547 - 'stats' => Cache::get_stats(),
548 - 'install_state' => $state,
648 + 'enabled' => $updated['cache_enabled'],
649 + // Surfaced at the top level so the dashboard can explain a
650 + // refusal rather than silently snapping the toggle back:
651 + // Cache::toggle() writes nothing when another plugin owns the
652 + // drop-in or WP_CACHE is in a shape we must not rewrite.
653 + 'blocked' => ! empty( $state['blocked'] ),
654 + 'blocked_reason' => $state['blocked_reason'] ?? null,
655 + 'stats' => Cache::get_stats(),
656 + 'install_state' => $state,
549 657 )
550 658 );
551 659 }
552 660 }