PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.2
1.4.2 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 All 36 releases
← All changes | includes/class-admin.php +39 -2 1.3.7 → 1.4.2 View file →
@@ -14,8 +14,15 @@
14 14 const PAGE_SLUG = 'xspeed';
15 15
16 16 const THEME_COOKIE = 'xspeed_theme';
17 17
18 + /**
19 + * The dashboard's sidebar offer reads a module's `purchase_needed` before
20 + * its `status_label`. An add-on checks for this constant before it labels
21 + * a setup step, which an older dashboard would read as "still for sale".
22 + */
23 + const OFFER_READS_PURCHASE_NEEDED = true;
24 +
18 25 public function __construct() {
19 26 add_action( 'admin_menu', array( $this, 'register_menu' ) );
20 27 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
21 28 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_menu_styles' ) );
@@ -195,8 +202,16 @@
195 202 // user inside the right module. WordPress's add_submenu_page
196 203 // strips the hash, so we inject directly into $submenu where
197 204 // it survives intact (the same trick Yoast / WooCommerce use
198 205 // for their per-area shortcuts).
206 + //
207 + // Writing to $submenu skips the capability check add_submenu_page()
208 + // does, and core keeps a top-level item the user can't open while its
209 + // submenu has entries. Without this guard an Editor saw an empty
210 + // "xSpeed Cache" flyout that led to a permissions error. (#720)
211 + if ( ! current_user_can( 'manage_options' ) ) {
212 + return;
213 + }
199 214 global $submenu;
200 215 $deep_links = self::deep_link_items();
201 216 foreach ( $deep_links as $hash => $label ) {
202 217 $submenu[ self::PAGE_SLUG ][] = array(
@@ -434,8 +449,12 @@
434 449 // Pro), so Pro filters this via `xspeed_pro_state`. Default:
435 450 // not_installed when Pro is absent; 'active' when Pro is present
436 451 // (Pro downgrades to 'unlicensed' when its license isn't valid).
437 452 'proState' => self::pro_state(),
453 + // The xCloud purge plugin is here: the site already has
454 + // Cloudflare Enterprise from its host, so the dashboard does
455 + // not offer the add-on.
456 + 'xcloudPurgePlugin' => Managed_Edge_Purge::installed(),
438 457 // Setup Wizard URL, surfaced in the sidebar profile popover.
439 458 'wizardUrl' => admin_url( 'admin.php?page=' . Onboarding::PAGE_SLUG ),
440 459 'bootstrap' => self::bootstrap_payload(),
441 460 )
@@ -531,8 +550,13 @@
531 550 // means the request-terminating nginx isn't user-editable on this
532 551 // host — the banner uses this to switch to honest messaging
533 552 // instead of dangling a snippet the user can't apply.
534 553 'behind_proxy' => Server::is_behind_proxy(),
554 + // Mirrors /status (see the note there). On first paint the
555 + // probe is whatever was last cached, so this is usually
556 + // `unknown` until the Health tab runs a live probe — the
557 + // dashboard needs the key present either way.
558 + 'rules' => Cache::rules_state( $probe ),
535 559 );
536 560 }
537 561
538 562 return array(
@@ -628,8 +652,13 @@
628 652 if ( empty( $schema ) && empty( $custom_panel ) ) {
629 653 continue;
630 654 }
631 655 $settings = Settings_Manager::get_public( $slug );
656 + // Something else on the site has taken over this module's job. It
657 + // stands down whatever its own switch says, so it is reported off,
658 + // with the reason in place of its usual one.
659 + $blocked = $module->blocked_by();
660 + $active = $module->is_active();
632 661
633 662 $entry = array(
634 663 'slug' => $slug,
635 664 'tier' => $module->tier(),
@@ -654,9 +683,9 @@
654 683 // must read THIS, not `enabled`, which only ever described the
655 684 // modules that happen to store that one key. null means the
656 685 // module has no meaningful on/off and should be excluded from
657 686 // any count rather than treated as off. (#363)
658 - 'active' => $module->is_active(),
687 + 'active' => ( null !== $blocked && true === $active ) ? false : $active,
659 688 // One sentence explaining the line above, computed next to it
660 689 // so the two cannot disagree. The UI shows it behind an (i)
661 690 // beside the status pill: "On" is a bare assertion otherwise,
662 691 // and least obvious exactly where it matters -- Media
@@ -661,9 +690,13 @@
661 690 // beside the status pill: "On" is a bare assertion otherwise,
662 691 // and least obvious exactly where it matters -- Media
663 692 // Optimization reads On while its two most prominent switches
664 693 // are off, because three other flags are on. (#363)
665 - 'active_reason' => $module->active_reason(),
694 + 'active_reason' => null !== $blocked ? $blocked : $module->active_reason(),
695 + // A purchase or licence the module still waits on. Shown in the
696 + // page header in place of the On/Off pill. Not while another
697 + // module has taken over: that reason is the one to show.
698 + 'status_label' => null !== $blocked ? null : $module->status_label(),
666 699 'label' => $meta['label'] ?? ucfirst( $slug ),
667 700 'icon' => $meta['icon'] ?? 'Square',
668 701 'description' => $meta['description'] ?? '',
669 702 // The dashboard group the module belongs to (cache, performance,
@@ -689,8 +722,12 @@
689 722 'setting_origins' => Settings_Manager::origins( $slug ),
690 723 'schema' => $schema,
691 724 'notices' => $module->ui_notices(),
692 725 'custom_panel' => $meta['custom_panel'] ?? null,
726 + // Why this module may not be switched on, when something else
727 + // on the site has taken over from it. Null for almost every
728 + // module almost always — Free never blocks anything itself.
729 + 'blocked_by' => $blocked,
693 730 );
694 731
695 732 /**
696 733 * Last-mile descriptor filter. Lets Pro (or third-party