Multisite: Leave Network Admin classic, and scope the session per site - #489
Draft
mmtr wants to merge 3 commits into
Draft
Multisite: Leave Network Admin classic, and scope the session per site#489mmtr wants to merge 3 commits into
mmtr wants to merge 3 commits into
Conversation
Two blocking bugs on a network install, both verified on a live subdirectory multisite. Network Admin was unreachable. `admin_init` fires there like on any other admin screen, so the portal redirect claimed it, but the target allowlist only accepts bare wp-admin filenames. `network/sites.php` never survived sanitization, the portal fell back to the admin home, and every `/wp-admin/network/*` and `/wp-admin/user/*` URL silently landed on the main site's desktop. The shell also had no menu it could address there: the dock is built from the network `$menu` while `openstation_menu_item_url()` resolves through `admin_url()`, so half the entries 404 and the rest point at the main site. `openstation_is_unsupported_admin_request()` gates the render path on both areas, the same way the per-request classic override does. The account preference is untouched; those screens just render classic. The window session was one network-global user meta shared by every site. Restoring on one site handed the shell another site's admin URLs, which the shell then iframes, and saving dropped every window belonging to the other site because they fail the same-origin admin check. Hopping between sites wiped the session each time. The key is per site now, with the main site and every single-site install keeping the bare historical key so no live data is orphaned. Windows whose URL is not same-origin admin for the current site are dropped on read as well as write, which also covers a session written before a domain remap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ultisite-compat-fcbeac # Conflicts: # docs/architecture.md
Network Admin opened inside a desktop window is a chromeless request, and that route works today: the shell's link interceptor puts `/wp-admin/network/*` in an iframe and the page renders. Only top-level navigation is gated. Every render-path check evaluates chromeless before the new gate, so that route is unaffected, but nothing held it in place. Both tests fail if the gate is moved ahead of the chromeless branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mmtr
marked this pull request as draft
August 6, 2026 11:04
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed changes
Top-level navigation to Network Admin (
/wp-admin/network/) and User Admin (/wp-admin/user/) now renders classic admin and stays at the requested URL. The shell no longer claims those requests: no portal redirect, no shell markup, noos-activebody class, no shell assets, and the shell-only admin-bar controls (fullscreen, overview, shortcuts, Mio) are hidden there. One predicate,openstation_is_unsupported_admin_request()inincludes/core/routing.php, gates all of it, so there is a single seam to change if the shell ever grows real Network Admin support.This does not touch the in-shell route. Opening Network Admin from the admin bar (My Sites > Network Admin) is a chromeless request, and chromeless is evaluated before the new gate everywhere, so that keeps working exactly as before: the page opens in a desktop window. Two tests pin it, and both fail if the gate is moved ahead of the chromeless branch.
Separately, the window session is now stored per site. On multisite the meta key carries the blog id (
desktop_mode_session_2), while the main site and every single-site install keep the existingdesktop_mode_session, so no live session is orphaned. Windows whose URL is not a same-origin admin URL for the current site are now dropped when the session is read, not only when it is written.Why are these changes being made?
A top-level request for a Network Admin URL never reached the screen it named.
admin_initfires there like on any other admin screen, so the portal redirect claimed it, but the portal's target allowlist only accepts bare wp-admin filenames:network/sites.phpnever survived sanitization and the portal fell back to its entry URL. Where that left you depended on your saved session, which made the bug easy to miss. With a network window already in the session you land on the network dashboard and it looks like it worked; with a fresh session you land on the main site's dashboard. Either way the deep link is gone, so a bookmark, a link from an email, or Core's own post-action redirect tonetwork/sites.php?updated=1all silently go somewhere else. The absentdesktop_mode_portal_intent=1flag on the resulting URL is the tell that the target was rejected.User meta is network-global, so a single session key meant every site on a network shared one blob. Restoring on one site handed the shell another site's admin URLs, which are same-origin and so get iframed into the wrong desktop. Saving dropped every window belonging to the other site, because they fail the same-origin admin check against the current site's
admin_url(). Hopping between sites wiped the session each time. The read-side filter covers the two cases where stored URLs no longer match: a network upgrading past this change, where the main site inherits the old shared blob, and a site whose admin URL moved, which native multisite domain mapping does as a supported operation.Testing instructions
Needs a multisite install. Throwaway lab, on a port that will not collide with your other wp-env instances:
Sign in at
http://localhost:8903/wp-login.phpasadmin/password.Top-level Network Admin and User Admin render classic
http://localhost:8903/wp-admin/network/sites.phpinto the address bar. Make sure you land on the Sites screen and the URL stays as typed. On trunk you end up on the network dashboard or the main site's desktop, never on Sites.network/,network/settings.php,network/plugins.php,network/users.phpand/wp-admin/user/. Make sure each renders at its own URL.network/sites.php, make sure the page looks like normal WP admin: network sidebar on the left, the sites table listing all three sites, Screen Options and Help tabs present. Nothing hidden or covered by a dark desktop.The in-shell route is unchanged (this is the regression risk, please do check it)
Site admin is unchanged
http://localhost:8903/wp-admin/. Make sure it still redirects into the desktop.http://localhost:8903/marketing/wp-admin/. Make sure the subsite desktop boots fully: dock, wallpaper, widget column, desktop icons, Trash icon.<link rel="manifest">points at/marketing/openstation/manifest.webmanifest, not the main site's.Session isolation
http://localhost:8903/marketing/wp-admin/. Make sure the desktop opens empty. On trunk you get the main site's windows restored here, pointing at/wp-admin/edit.php.http://localhost:8903/wp-admin/. Make sure your original two windows come back, and that marketing's window is not among them. On trunk the two are gone and marketing's single window shows instead./marketing/wp-admin/once more and make sure its one window is still there.npx @wordpress/env run cli wp user meta list 1 --keys=desktop_mode_session,desktop_mode_session_2. Make sure both keys exist and hold different window lists.Single-site key is untouched
On a single-site install, save a session, then run
wp user meta list 1 --keys=desktop_mode_session. Make sure the key is still the baredesktop_mode_sessionand that a session created before this branch restores unchanged.Upgrade path, simulating a network that ran the old shared key
Load the main site's desktop. Make sure Media and the Recycle Bin restore, and the marketing window does not.
Automated
npm run build,npm run lint,npm run typecheck,npm run test:js(3789),npm run test:php(2054),npm run lint:php. All green.To exercise the multisite-only test, add
<php><const name="WP_TESTS_MULTISITE" value="1"/></php>to a copy oftests/phpunit/phpunit.xml.distand run--filter 'Tests_OpenStation_(UnsupportedAdmin|Session|Portal)'against it (104 pass). Note that a full multisite run has 22 pre-existing failures inpluginsWindowRegistrationandupdateNotice, unrelated to this change: they assert capabilities Core denies site administrators on a network. That is why there is no multisite CI job in this PR.🤖 Generated with Claude Code