Skip to content

Multisite: Leave Network Admin classic, and scope the session per site - #489

Draft
mmtr wants to merge 3 commits into
trunkfrom
claude/openstation-multisite-compat-fcbeac
Draft

Multisite: Leave Network Admin classic, and scope the session per site#489
mmtr wants to merge 3 commits into
trunkfrom
claude/openstation-multisite-compat-fcbeac

Conversation

@mmtr

@mmtr mmtr commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

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, no os-active body 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() in includes/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 existing desktop_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_init fires 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.php never 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 to network/sites.php?updated=1 all silently go somewhere else. The absent desktop_mode_portal_intent=1 flag 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:

mkdir ms-lab && cd ms-lab
cat > .wp-env.json <<'JSON'
{ "core": null, "multisite": true, "port": 8903, "testsPort": 8904,
  "plugins": [ "/absolute/path/to/openstation" ],
  "config": { "WP_DEBUG": true, "WP_DEBUG_LOG": true, "WP_DEBUG_DISPLAY": false } }
JSON
npx @wordpress/env start
npx @wordpress/env run cli wp site create --slug=marketing --title=Marketing --email=siteadmin@example.com
npx @wordpress/env run cli wp plugin activate openstation --network
npx @wordpress/env run cli wp user meta update 1 desktop_mode_mode 1

Sign in at http://localhost:8903/wp-login.php as admin / password.

Top-level Network Admin and User Admin render classic

  1. Type http://localhost:8903/wp-admin/network/sites.php into 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.
  2. Repeat for network/, network/settings.php, network/plugins.php, network/users.php and /wp-admin/user/. Make sure each renders at its own URL.
  3. On 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.
  4. Make sure the admin bar there reads "Switch to OpenStation", and that the fullscreen, overview, keyboard-shortcuts and Mio buttons are absent.
  5. Click "Switch to OpenStation". Make sure it takes you to the main site's desktop.

The in-shell route is unchanged (this is the regression risk, please do check it)

  1. From the main site's desktop, open the admin bar's My Sites > Network Admin > Dashboard. Make sure it opens in a desktop window and shows the network dashboard ("You have N sites and M users"), same as on trunk.
  2. Do the same for Sites, Users, Themes, Plugins and Settings under that menu. Make sure each opens in a window and renders the network screen, with no admin sidebar inside the window.
  3. Make sure none of those windows shows the whole desktop shell nested inside itself.

Site admin is unchanged

  1. Go to http://localhost:8903/wp-admin/. Make sure it still redirects into the desktop.
  2. Go to http://localhost:8903/marketing/wp-admin/. Make sure the subsite desktop boots fully: dock, wallpaper, widget column, desktop icons, Trash icon.
  3. Open a native window there (Recycle Bin, or Posts from the dock). Make sure it opens and loads.
  4. View source and make sure <link rel="manifest"> points at /marketing/openstation/manifest.webmanifest, not the main site's.
  5. Load any single-site install with this branch and make sure nothing about the desktop changed.

Session isolation

  1. On the main site's desktop, open two windows: Posts and Media.
  2. Navigate to 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.
  3. Open one window on marketing: Posts.
  4. Navigate back to 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.
  5. Go back to /marketing/wp-admin/ once more and make sure its one window is still there.
  6. Check storage: 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 bare desktop_mode_session and that a session created before this branch restores unchanged.

Upgrade path, simulating a network that ran the old shared key

npx @wordpress/env run cli wp eval 'update_user_meta( 1, "desktop_mode_session", array(
  "windows" => array(
    array( "id"=>"a", "url"=>"http://localhost:8903/marketing/wp-admin/edit.php", "state"=>"normal", "desktopId"=>"desktop-1" ),
    array( "id"=>"b", "url"=>"http://localhost:8903/wp-admin/upload.php", "state"=>"normal", "desktopId"=>"desktop-1" ),
    array( "id"=>"desktop-mode-recycle-bin", "url"=>"#desktop-mode-recycle-bin", "state"=>"normal", "desktopId"=>"desktop-1" ),
  ),
  "desktops" => array( array("id"=>"desktop-1","label"=>"Desktop 1") ),
  "activeDesktop" => "desktop-1", "focused"=>"", "updated"=>999,
) );'

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 of tests/phpunit/phpunit.xml.dist and run --filter 'Tests_OpenStation_(UnsupportedAdmin|Session|Portal)' against it (104 pass). Note that a full multisite run has 22 pre-existing failures in pluginsWindowRegistration and updateNotice, 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

Open WordPress Playground Preview

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>
@mmtr mmtr self-assigned this Aug 4, 2026
mmtr and others added 2 commits August 5, 2026 15:43
…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
mmtr marked this pull request as draft August 6, 2026 11:04
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