Skip to content

Multisite: Fix My Sites admin bar menu for large networks.#11259

Open
soderlind wants to merge 1 commit intoWordPress:trunkfrom
soderlind:fix/my-sites
Open

Multisite: Fix My Sites admin bar menu for large networks.#11259
soderlind wants to merge 1 commit intoWordPress:trunkfrom
soderlind:fix/my-sites

Conversation

@soderlind
Copy link

Multisite: Fix My Sites admin bar menu for large networks

Trac ticket: #15317 (open since 2010)
Based on: Learnings from the my-sites-fix proof-of-concept plugin and the Super Admin All Sites Menu plugin.


The problem

The "My Sites" dropdown in the WordPress admin bar has three long-standing issues on multisite networks:

  1. Super admins only see sites they are explicitly added to. get_blogs_of_user() discovers sites by scanning user meta keys ending in _capabilities. A super admin has implicit access to all sites but only has explicit capability rows for sites they were individually added to — so the menu is missing most sites.

  2. The dropdown can't scroll past the viewport. When the site list is longer than the browser window, items below the fold are simply unreachable. There is no max-height or overflow on the submenu wrapper. This is the original bug reported in #15317.

  3. switch_to_blog() is called for every site. Both wp_admin_bar_my_sites_menu() and get_blogs_of_user() call switch_to_blog() per site — the former explicitly in a loop, the latter implicitly via WP_Site::get_details() when accessing $site->blogname and $site->siteurl. On a network with hundreds of sites this is a significant performance hit.

What this patch changes

Files changed

File Summary
user.php Super admin branch in get_blogs_of_user(); batch option fetch; new _batch_get_site_options() helper
admin-bar.php Rewrite per-site loop in wp_admin_bar_my_sites_menu()
admin-bar.css Scrollable dropdown + fixed-position fly-outs
admin-bar.js Fly-out positioning with viewport clamping

Fix 1 — Show all network sites for super admins

In get_blogs_of_user(), when is_super_admin( $user_id ) is true, the function now calls get_sites() (ordered by path, excluding archived/spam/deleted/mature) instead of scanning _capabilities meta keys. This ensures the admin bar and the My Sites admin page show every site on the network.

Fix 2 — Make the dropdown scrollable

A new @media screen and (min-width: 783px) block in admin-bar.css adds:

#wpadminbar .ab-top-menu > li#wp-admin-bar-my-sites > .ab-sub-wrapper {
    max-height: calc(100vh - var(--wp-admin--admin-bar--height, 32px));
    overflow-y: auto;
}

This makes the My Sites submenu scrollable when it exceeds the viewport height. The mobile menu (below 783px) is unaffected.

Caveat: overflow-y: auto clips the nested fly-out submenus (Dashboard, New Post, etc.) that normally position themselves with margin-left: 100%. The fix switches those fly-outs to position: fixed and computes their coordinates in JavaScript using getBoundingClientRect(). The JS also clamps the fly-out's top position so it never extends below the viewport, and supports RTL layouts by positioning to the left of the parent item when document.documentElement.dir === 'rtl'.

Fix 3 — Eliminate switch_to_blog() overhead

In the admin bar menu loop: wp_admin_bar_my_sites_menu() no longer calls switch_to_blog() per site. URLs are computed directly from the blog object properties ($blog->siteurl, $blog->domain, $blog->path). Per-site current_user_can() checks are removed — these were guarding convenience links (Dashboard, New Post, Manage Comments) whose target pages enforce their own permissions.

switch_to_blog() is still called when site icons are enabled, since has_site_icon() and get_site_icon_url() require switched context. To mitigate the cost, the wp_admin_bar_show_site_icons filter now defaults to false for networks with more than 20 sites (previously it defaulted to true unconditionally). The filter remains available for plugins to override.

In get_blogs_of_user(): The magic property accesses $site->blogname and $site->siteurl on WP_Site objects triggered WP_Site::get_details(), which internally calls switch_to_blog() for each site. This is replaced by a new private helper function _batch_get_site_options() that builds a single UNION ALL query across per-site wp_N_options tables to fetch blogname and siteurl for all sites at once. Table names are derived from $wpdb->get_blog_prefix() (a pure function with no side effects), and option names are passed through $wpdb->prepare().

Backward compatibility notes

  • The pre_get_blogs_of_user filter (since 4.6.0) continues to work and can override the super admin behavior.
  • The get_blogs_of_user filter continues to fire on the result.
  • The wp_admin_bar_show_site_icons filter (since 6.0.0) still controls site icon visibility; the only change is the default value now considers site count.
  • The myblogs_blog_actions filter in my-sites.php is not affected — that page's render loop still uses switch_to_blog() to preserve backward compatibility for plugins relying on switched context.
  • The _batch_get_site_options() function is marked @access private and is not intended for external use.

Testing

  1. Multisite with < 15 sites: Dropdown should not scroll unless needed; fly-outs appear correctly.
  2. Multisite with > 25 sites: All sites reachable via scroll; fly-outs clamp within viewport.
  3. Super admin vs regular user: Super admin sees all network sites; regular user sees only their sites.
  4. Performance: Verify no switch_to_blog() calls during menu render (except when site icons are enabled for ≤ 20 sites). Check via Query Monitor or $GLOBALS['_wp_switched_stack'].
  5. RTL: Fly-out submenus should appear to the left of the parent item.
  6. Mobile: Menu unchanged below 783px viewport width.
  7. No-JS fallback: Scroll works (CSS-only); fly-out positioning falls back to top: 0; left: 0.

Trac ticket: https://core.trac.wordpress.org/ticket/15317

Use of AI Tools

GitHub Copilot + Opus 4.6 was used to review this fix


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

Three fixes for the My Sites dropdown in the WordPress admin bar:

1. Show all network sites for super admins -- get_blogs_of_user() only
   returned sites with explicit _capabilities meta rows. Super admins
   now get all sites via get_sites().

2. Make the dropdown scrollable (Trac #15317) -- adds max-height and
   overflow-y: auto so the menu is reachable when the site list exceeds
   the viewport. Fly-out submenus use position: fixed with JS-computed
   coordinates to escape the scroll container's overflow clipping.

3. Eliminate switch_to_blog() overhead -- the per-site loop in
   wp_admin_bar_my_sites_menu() now computes URLs directly from the
   blog object. get_blogs_of_user() batch-fetches blogname and
   siteurl via a single UNION ALL query instead of triggering hidden
   switch_to_blog() calls through WP_Site::get_details().

Props PerS, ocean90, SergeyBiryukov, jeremyfelt, morganestes, trepmal,
      austinginder, zephyr7501, sabernhardt, paaljoachim.
Fixes #15317.
@github-actions
Copy link

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props pers.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@soderlind
Copy link
Author

@westonruter ^^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant