| @@ -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( |
| @@ -551,9 +575,18 @@ | ||
| 551 | 575 | // /status block so the dashboard callout renders on first paint |
| 552 | 576 | // without waiting for a status re-fetch. |
| 553 | 577 | 'mobile_separate' => array( |
| 554 | 578 | 'enabled' => (bool) ( $opts['cache_enabled'] ? ( Settings_Manager::get( 'cache' )['mobile_separate'] ?? false ) : false ), |
| 555 | - 'blocking' => $rewrite_capable && 'mobile_separate' === Cache::static_rewrite_block_reason(), | |
| 579 | + // Gated to servers that HAVE a static fast path — on IIS or | |
| 580 | + // an undetected server block_reason still falls through to | |
| 581 | + // mobile_separate, and reporting that as "blocking" would | |
| 582 | + // nag about a rewrite that does not exist there (#108). | |
| 583 | + // LiteSpeed joined the capable set with the Static Fast | |
| 584 | + // Path opt-in (#509); its own refusal (litespeed_dropin) | |
| 585 | + // outranks mobile_separate, so this stays false there | |
| 586 | + // until the opt-in is on. | |
| 587 | + 'blocking' => ( $rewrite_capable || Server::LITESPEED === $server_type ) | |
| 588 | + && 'mobile_separate' === Cache::static_rewrite_block_reason(), | |
| 556 | 589 | 'needs_review' => Cache::mobile_separate_needs_review(), |
| 557 | 590 | ), |
| 558 | 591 | // One consolidated nginx server-block snippet aggregating |
| 559 | 592 | // every enabled module's directives (Cache static-rewrite, |
| @@ -619,8 +652,13 @@ | ||
| 619 | 652 | if ( empty( $schema ) && empty( $custom_panel ) ) { |
| 620 | 653 | continue; |
| 621 | 654 | } |
| 622 | 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(); | |
| 623 | 661 | |
| 624 | 662 | $entry = array( |
| 625 | 663 | 'slug' => $slug, |
| 626 | 664 | 'tier' => $module->tier(), |
| @@ -645,9 +683,9 @@ | ||
| 645 | 683 | // must read THIS, not `enabled`, which only ever described the |
| 646 | 684 | // modules that happen to store that one key. null means the |
| 647 | 685 | // module has no meaningful on/off and should be excluded from |
| 648 | 686 | // any count rather than treated as off. (#363) |
| 649 | - 'active' => $module->is_active(), | |
| 687 | + 'active' => ( null !== $blocked && true === $active ) ? false : $active, | |
| 650 | 688 | // One sentence explaining the line above, computed next to it |
| 651 | 689 | // so the two cannot disagree. The UI shows it behind an (i) |
| 652 | 690 | // beside the status pill: "On" is a bare assertion otherwise, |
| 653 | 691 | // and least obvious exactly where it matters -- Media |
| @@ -652,12 +690,23 @@ | ||
| 652 | 690 | // beside the status pill: "On" is a bare assertion otherwise, |
| 653 | 691 | // and least obvious exactly where it matters -- Media |
| 654 | 692 | // Optimization reads On while its two most prominent switches |
| 655 | 693 | // are off, because three other flags are on. (#363) |
| 656 | - '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(), | |
| 657 | 699 | 'label' => $meta['label'] ?? ucfirst( $slug ), |
| 658 | 700 | 'icon' => $meta['icon'] ?? 'Square', |
| 659 | 701 | 'description' => $meta['description'] ?? '', |
| 702 | + // The dashboard group the module belongs to (cache, performance, | |
| 703 | + // network, insights, tools, ai-agents, settings), declared by the | |
| 704 | + // module itself. The Hub kept its own copy of this map, so every | |
| 705 | + // new module landed in its "Other" bucket until the Hub shipped. | |
| 706 | + // Must agree with src/components/sidebarGroups.ts; a unit test | |
| 707 | + // holds the two together. | |
| 708 | + 'group' => $meta['group'] ?? null, | |
| 660 | 709 | // Short label for the module's own tab when it hosts a tabbed |
| 661 | 710 | // page (FBS-83633). Only set on host modules. |
| 662 | 711 | 'tab_label' => $meta['tab_label'] ?? null, |
| 663 | 712 | // Public view: real values except secret fields, which are masked. |
| @@ -673,8 +722,12 @@ | ||
| 673 | 722 | 'setting_origins' => Settings_Manager::origins( $slug ), |
| 674 | 723 | 'schema' => $schema, |
| 675 | 724 | 'notices' => $module->ui_notices(), |
| 676 | 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, | |
| 677 | 730 | ); |
| 678 | 731 | |
| 679 | 732 | /** |
| 680 | 733 | * Last-mile descriptor filter. Lets Pro (or third-party |