PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
← All changes | includes/core/routing.php +320 -90 0.8.71.1.10 View file →
@@ -1,10 +1,10 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — request routing helpers.
3 + * OpenStation — request routing helpers.
4 4 *
5 5 * Chromeless / classic admin-bar suppression and the
6 - * `wp_redirect` filter pair that re-stamps the desktop-mode
6 + * `wp_redirect` filter pair that re-stamps the openstation
7 7 * flags onto server-built redirects. Extracted from the
8 8 * 1,609-LOC `helpers.php` during the architecture-0.8.1 PHP
9 9 * slicing (phase 6).
10 10 *
@@ -14,21 +14,22 @@
14 14 * file before `helpers.php`, so the function definitions are
15 15 * always present by the time WordPress wants them.
16 16 *
17 17 * Functions in this file:
18 - * - {@see desktop_mode_chromeless_hide_admin_bar()} — `show_admin_bar` filter
19 - * - {@see desktop_mode_chromeless_suppress_admin_bar()} — `admin_init` action
20 - * - {@see desktop_mode_chromeless_preserve_redirect()} — `wp_redirect` filter
21 - * - {@see desktop_mode_classic_preserve_redirect()} — `wp_redirect` filter
22 - * - {@see desktop_mode_is_admin_redirect_target()} — internal predicate
18 + * - {@see openstation_url_is_same_admin()} — same-origin admin URL predicate
19 + * - {@see openstation_url_is_page_less_admin_php()} — "renders nothing" predicate
20 + * - {@see openstation_resolve_admin_target()} — admin filename → URL resolver
21 + * - {@see openstation_admin_target_allowlist()} — wp-admin filename allowlist
22 + * - {@see openstation_is_chromeless_request()} — chromeless request detection
23 + * - {@see openstation_is_classic_request()} — classic-override request detection
24 + * - {@see openstation_is_subresource_request()} — sub-resource fetch detection
25 + * - {@see openstation_chromeless_hide_admin_bar()} — `show_admin_bar` filter
26 + * - {@see openstation_chromeless_suppress_admin_bar()} — `admin_init` action
27 + * - {@see openstation_chromeless_preserve_redirect()} — `wp_redirect` filter
28 + * - {@see openstation_classic_preserve_redirect()} — `wp_redirect` filter
29 + * - {@see openstation_is_admin_redirect_target()} — internal predicate
23 30 *
24 - * The chromeless / classic *request-detection* helpers
25 - * (`desktop_mode_is_chromeless_request()`,
26 - * `desktop_mode_is_classic_request()`) still live in
27 - * `helpers.php` for now — moving them is the next phase-6 cut.
28 - *
29 - * @package Desktop_Mode
30 - * @since 0.8.1
31 + * @package OpenStation
31 32 */
32 33
33 34 defined( 'ABSPATH' ) || exit;
34 35
@@ -42,14 +43,12 @@
42 43 *
43 44 * An empty string returns false — a missing URL is never
44 45 * "same-origin admin" for the purposes of any caller.
45 46 *
46 - * @since 0.11.0
47 - *
48 47 * @param string $url URL to test.
49 48 * @return bool
50 49 */
51 -function desktop_mode_url_is_same_admin( $url ) {
50 +function openstation_url_is_same_admin( $url ) {
52 51 if ( ! is_string( $url ) || '' === $url ) {
53 52 return false;
54 53 }
55 54
@@ -77,8 +76,52 @@
77 76 return 0 === strpos( $url_path, $admin_path );
78 77 }
79 78
80 79 /**
80 + * Whether `$url` addresses `wp-admin/admin.php` with no `page` arg.
81 + *
82 + * `admin.php` is core's plugin-screen bootstrap, and the allowlist in
83 + * {@see openstation_admin_target_allowlist()} accepts it for exactly
84 + * that reason — every plugin screen in the admin lives there. Without
85 + * a `page` arg, though, there is no screen to dispatch to: core falls
86 + * through the last `else` in `wp-admin/admin.php`, fires a couple of
87 + * back-compat `load-*` hooks, and returns 200 with an empty body,
88 + * having required neither `admin-header.php` nor `admin-footer.php`.
89 + *
90 + * So the URL resolves, passes every same-origin and allowlist check,
91 + * and renders nothing. Callers that are about to turn a URL into a
92 + * window or a redirect target use this to refuse it and fall back.
93 + *
94 + * Accepts absolute URLs and request-URI-shaped paths, mirroring
95 + * {@see openstation_url_is_shell_screen()}, whose guard this sits
96 + * beside at every call site.
97 + *
98 + * @param string $url URL or path to test.
99 + * @return bool
100 + */
101 +function openstation_url_is_page_less_admin_php( $url ) {
102 + if ( ! is_string( $url ) || '' === $url ) {
103 + return false;
104 + }
105 +
106 + $path = wp_parse_url( $url, PHP_URL_PATH );
107 + if ( ! is_string( $path ) || 'admin.php' !== basename( $path ) ) {
108 + return false;
109 + }
110 +
111 + $query = wp_parse_url( $url, PHP_URL_QUERY );
112 + if ( ! is_string( $query ) || '' === $query ) {
113 + return true;
114 + }
115 +
116 + // `page=` present but empty is the same nothing: core only sets
117 + // `$plugin_page` from a non-empty `?page=`. An array (`page[]=x`)
118 + // is not a slug either.
119 + parse_str( $query, $args );
120 + return ! isset( $args['page'] ) || ! is_string( $args['page'] ) || '' === $args['page'];
121 +}
122 +
123 +/**
81 124 * Resolves an admin-page filename (e.g. `edit.php`) to its
82 125 * absolute admin URL, allowlisted against the canonical set of
83 126 * wp-admin top-level filenames.
84 127 *
@@ -87,18 +130,17 @@
87 130 * exist in the static allowlist. A regex-only check would accept
88 131 * `custom_admin_page.php` if a plugin named something that way;
89 132 * the explicit allowlist closes that.
90 133 *
91 - * @since 0.11.0
92 - *
93 - * @param string $file Bare admin filename (no path, no query string).
134 + * @param string $file Bare admin filename (no path, no query string).
135 + * @param bool $network Resolve against the network admin's own screens.
94 136 * @return string|WP_Error Absolute admin URL on success, `WP_Error` otherwise.
95 137 */
96 -function desktop_mode_resolve_admin_target( $file ) {
138 +function openstation_resolve_admin_target( $file, $network = false ) {
97 139 $file = is_string( $file ) ? trim( $file ) : '';
98 140 if ( '' === $file ) {
99 141 return new WP_Error(
100 - 'desktop_mode_empty_target',
142 + 'openstation_empty_target',
101 143 __( 'Admin target cannot be empty.', 'desktop-mode' )
102 144 );
103 145 }
104 146
@@ -103,9 +145,9 @@
103 145 }
104 146
105 147 if ( false !== strpos( $file, '..' ) || false !== strpos( $file, '/' ) || false !== strpos( $file, '\\' ) ) {
106 148 return new WP_Error(
107 - 'desktop_mode_invalid_target',
149 + 'openstation_invalid_target',
108 150 __( 'Admin target contains invalid path characters.', 'desktop-mode' )
109 151 );
110 152 }
111 153
@@ -114,16 +156,25 @@
114 156 // below is the final arbiter; this regex just pre-filters
115 157 // clearly bad inputs cheaply.
116 158 if ( ! preg_match( '/^[a-z0-9_-]+\.php$/i', $file ) ) {
117 159 return new WP_Error(
118 - 'desktop_mode_invalid_target',
160 + 'openstation_invalid_target',
119 161 __( 'Admin target must be a plain .php filename.', 'desktop-mode' )
120 162 );
121 163 }
122 164
123 - if ( ! in_array( strtolower( $file ), desktop_mode_admin_target_allowlist(), true ) ) {
165 + if ( $network ) {
166 + return in_array( strtolower( $file ), openstation_network_admin_target_allowlist(), true )
167 + ? network_admin_url( $file )
168 + : new WP_Error(
169 + 'openstation_unknown_target',
170 + __( 'Admin target does not exist.', 'desktop-mode' )
171 + );
172 + }
173 +
174 + if ( ! in_array( strtolower( $file ), openstation_admin_target_allowlist(), true ) ) {
124 175 return new WP_Error(
125 - 'desktop_mode_unknown_target',
176 + 'openstation_unknown_target',
126 177 __( 'Admin target does not exist.', 'desktop-mode' )
127 178 );
128 179 }
129 180
@@ -130,10 +181,47 @@
130 181 return admin_url( $file );
131 182 }
132 183
133 184 /**
185 + * Canonical `wp-admin/network/` filenames a target may resolve to.
186 + *
187 + * The network admin's own screens, and only those: the site allowlist
188 + * cannot stand in for it, since the two directories share filenames
189 + * that mean different things (`users.php` is everyone on the network
190 + * here, one site's users there).
191 + *
192 + * @return string[]
193 + */
194 +function openstation_network_admin_target_allowlist() {
195 + return array(
196 + 'index.php',
197 + 'sites.php',
198 + 'site-new.php',
199 + 'site-info.php',
200 + 'site-users.php',
201 + 'site-themes.php',
202 + 'site-settings.php',
203 + 'users.php',
204 + 'user-new.php',
205 + 'themes.php',
206 + 'theme-install.php',
207 + 'plugins.php',
208 + 'plugin-install.php',
209 + 'plugin-editor.php',
210 + 'settings.php',
211 + 'setup.php',
212 + 'upgrade.php',
213 + 'update-core.php',
214 + 'about.php',
215 + 'credits.php',
216 + 'freedoms.php',
217 + 'privacy.php',
218 + );
219 +}
220 +
221 +/**
134 222 * Returns the allowlist of canonical wp-admin top-level
135 - * filenames that {@see desktop_mode_resolve_admin_target()}
223 + * filenames that {@see openstation_resolve_admin_target()}
136 224 * accepts.
137 225 *
138 226 * Hardcoded rather than read from disk so the plugin doesn't
139 227 * depend on a particular WordPress install layout (and doesn't
@@ -140,13 +228,11 @@
140 228 * reference `ABSPATH` to probe core files). Plugins that ship
141 229 * their own top-level admin pages (rare) can extend the list
142 230 * via the filter.
143 231 *
144 - * @since 0.6.2
145 - *
146 232 * @return string[] Lowercased filenames including extension.
147 233 */
148 -function desktop_mode_admin_target_allowlist() {
234 +function openstation_admin_target_allowlist() {
149 235 $files = array(
150 236 'about.php',
151 237 'admin-ajax.php',
152 238 'admin-footer.php',
@@ -239,13 +325,11 @@
239 325 /**
240 326 * Filters the wp-admin filename allowlist used when resolving
241 327 * portal `target=` query args.
242 328 *
243 - * @since 0.6.2
244 - *
245 329 * @param string[] $files Default allowlist.
246 330 */
247 - $files = (array) apply_filters( 'desktop_mode_admin_target_allowlist', $files );
331 + $files = (array) apply_filters( 'openstation_admin_target_allowlist', $files );
248 332
249 333 return array_values( array_unique( array_map( 'strtolower', array_filter( $files, 'is_string' ) ) ) );
250 334 }
251 335
@@ -251,21 +335,19 @@
251 335
252 336 /**
253 337 * Checks whether the current request is a chromeless request.
254 338 *
255 - * Chromeless requests are admin pages loaded inside desktop-mode
339 + * Chromeless requests are admin pages loaded inside openstation
256 340 * windows (iframes). They render only the page content without
257 341 * the admin shell (sidebar, admin bar, footer).
258 342 *
259 - * @since 0.1.0
260 - *
261 343 * @return bool True if this is a chromeless (iframe) request.
262 344 */
263 -function desktop_mode_is_chromeless_request() {
264 - if ( ! desktop_mode_is_enabled() ) {
345 +function openstation_is_chromeless_request() {
346 + if ( ! openstation_is_enabled() ) {
265 347 // Only allow chromeless mode if the user actually has
266 - // desktop mode enabled. Prevents stripping admin chrome via
267 - // a bare `?desktop_mode_chromeless=1` parameter from a
348 + // OpenStation enabled. Prevents stripping admin chrome via
349 + // a bare `?openstation_chromeless=1` parameter from a
268 350 // logged-out URL.
269 351 return false;
270 352 }
271 353
@@ -271,9 +353,9 @@
271 353
272 354 // Primary signal — the explicit query flag the parent shell
273 355 // adds when opening windows.
274 356 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request flag, no state change.
275 - if ( ! empty( $_GET['desktop_mode_chromeless'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['desktop_mode_chromeless'] ) ) ) {
357 + if ( ! empty( $_GET['openstation_chromeless'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['openstation_chromeless'] ) ) ) {
276 358 return true;
277 359 }
278 360
279 361 // Fallback signal — the request is a same-origin iframe load.
@@ -281,12 +363,12 @@
281 363 // the `Sec-Fetch-*` headers reliably, and they are immune to
282 364 // JavaScript spoofing (the browser sets them itself).
283 365 //
284 366 // This catches the failure mode where an internal admin
285 - // navigation drops the `?desktop_mode_chromeless=1` query flag —
367 + // navigation drops the `?openstation_chromeless=1` query flag —
286 368 // Gutenberg's `window.location` assignments, meta-refresh
287 369 // redirects, or any link the inline rewriter missed. The user
288 - // is in an iframe on the same origin, has desktop mode enabled,
370 + // is in an iframe on the same origin, has OpenStation enabled,
289 371 // so render as chromeless.
290 372 //
291 373 // `Sec-Fetch-Site: same-origin` is the cross-origin guard so a
292 374 // foreign site that iframes the wp-admin page can't trick us
@@ -300,18 +382,15 @@
300 382 : '';
301 383 if ( 'iframe' === $fetch_dest && 'same-origin' === $fetch_site ) {
302 384 /**
303 385 * Filter the Sec-Fetch fallback. Return false to require an
304 - * explicit `?desktop_mode_chromeless=1` flag (matches
305 - * pre-0.18 behaviour); useful for environments where a
386 + * explicit `?openstation_chromeless=1` flag; useful for environments where a
306 387 * reverse proxy strips the `Sec-Fetch-*` headers and they
307 388 * can't be trusted.
308 389 *
309 - * @since 0.18.0
310 - *
311 390 * @param bool $allow Default true.
312 391 */
313 - return (bool) apply_filters( 'desktop_mode_chromeless_sec_fetch_fallback', true );
392 + return (bool) apply_filters( 'openstation_chromeless_sec_fetch_fallback', true );
314 393 }
315 394
316 395 return false;
317 396 }
@@ -322,33 +401,62 @@
322 401 *
323 402 * The window-chrome "Detach" action opens an admin page in a new
324 403 * browser tab with `?desktop_mode_classic=1` so the user can view
325 404 * that one page outside the desktop shell without disabling
326 - * desktop mode account-wide. The flag is a per-request override:
327 - * `desktop_mode_is_enabled()` still returns true (the user's
405 + * OpenStation account-wide. The flag is a per-request override:
406 + * `openstation_is_enabled()` still returns true (the user's
328 407 * preference hasn't changed), but the shell, shell assets, and
329 408 * body class are skipped for this request so the classic admin
330 409 * renders normally.
331 410 *
332 - * Keep this separate from `desktop_mode_is_enabled()` so the
411 + * Keep this separate from `openstation_is_enabled()` so the
333 412 * admin-bar toggle in the detached tab correctly reflects the
334 - * account state — letting the user disable desktop mode entirely
413 + * account state — letting the user disable OpenStation entirely
335 414 * from the tab if they want to.
336 415 *
337 - * @since 0.4.0
338 - *
339 416 * @return bool True if the request carries `?desktop_mode_classic=1`.
340 417 */
341 -function desktop_mode_is_classic_request() {
418 +function openstation_is_classic_request() {
342 419 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request flag.
343 - if ( empty( $_GET[ DESKTOP_MODE_CLASSIC_FLAG ] ) ) {
420 + if ( empty( $_GET[ OPENSTATION_CLASSIC_FLAG ] ) ) {
344 421 return false;
345 422 }
346 423 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request flag.
347 - return '1' === sanitize_text_field( wp_unslash( $_GET[ DESKTOP_MODE_CLASSIC_FLAG ] ) );
424 + return '1' === sanitize_text_field( wp_unslash( $_GET[ OPENSTATION_CLASSIC_FLAG ] ) );
348 425 }
349 426
350 427 /**
428 + * Checks whether the browser is fetching this request as a
429 + * sub-resource of some page rather than navigating to it.
430 + *
431 + * Admin URLs serve more than pages. Jetpack's admin-bar sparkline is
432 + * an `<img>` whose src is `admin.php?page=stats&noheader&proxy&chart=…`:
433 + * core's `admin.php` skips the header on `noheader` and the page hook
434 + * echoes PNG bytes. The Jetpack Stats screen loads its report body the
435 + * same way, over XHR. Treating such a request as "a user landing on a
436 + * plain admin page" and forwarding it into the desktop hands the
437 + * consumer an HTML document instead: the admin bar then draws a broken
438 + * image with the alt text where the chart should be.
439 + *
440 + * `Sec-Fetch-Mode` is the browser's own answer, set by the user agent
441 + * and immune to script. `navigate` is a document or frame load, the
442 + * only kind of request worth forwarding into the desktop; `cors`,
443 + * `no-cors`, `same-origin` and `websocket` are sub-resource fetches.
444 + * A missing header (an old browser, a proxy that strips it) answers
445 + * false: not known to be a sub-resource, so callers keep behaving as
446 + * they always did.
447 + *
448 + * @return bool True when the request is a sub-resource fetch.
449 + */
450 +function openstation_is_subresource_request() {
451 + if ( empty( $_SERVER['HTTP_SEC_FETCH_MODE'] ) ) {
452 + return false;
453 + }
454 + $mode = strtolower( sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_FETCH_MODE'] ) ) );
455 + return '' !== $mode && 'navigate' !== $mode;
456 +}
457 +
458 +/**
351 459 * Disables the admin bar on chromeless (iframe) requests.
352 460 *
353 461 * Hooked on the `show_admin_bar` filter so the front-end bar path
354 462 * also sees a false return. In admin, `is_admin_bar_showing()`
@@ -353,23 +461,21 @@
353 461 * Hooked on the `show_admin_bar` filter so the front-end bar path
354 462 * also sees a false return. In admin, `is_admin_bar_showing()`
355 463 * short-circuits to true for any `is_admin()` request regardless
356 464 * of this filter, so the actual render is stopped by
357 - * {@see desktop_mode_chromeless_suppress_admin_bar()} below; this
465 + * {@see openstation_chromeless_suppress_admin_bar()} below; this
358 466 * filter is kept for completeness + tests.
359 467 *
360 - * @since 0.1.0
361 - *
362 468 * @param bool $show Whether the admin bar should be shown.
363 469 * @return bool
364 470 */
365 -function desktop_mode_chromeless_hide_admin_bar( $show ) {
366 - if ( desktop_mode_is_chromeless_request() ) {
471 +function openstation_chromeless_hide_admin_bar( $show ) {
472 + if ( openstation_is_chromeless_request() ) {
367 473 return false;
368 474 }
369 475 return $show;
370 476 }
371 -add_filter( 'show_admin_bar', 'desktop_mode_chromeless_hide_admin_bar' );
477 +add_filter( 'show_admin_bar', 'openstation_chromeless_hide_admin_bar' );
372 478
373 479 /**
374 480 * Suppresses the admin bar render inside chromeless iframes.
375 481 *
@@ -377,21 +483,151 @@
377 483 * context, so the `show_admin_bar` filter alone can't stop
378 484 * `wp_admin_bar_render()` from firing on `in_admin_header`. We
379 485 * detach the render action instead and let chromeless.css hide
380 486 * the `wp-toolbar` padding on `<html>`.
381 - *
382 - * @since 0.1.0
383 487 */
384 -function desktop_mode_chromeless_suppress_admin_bar() {
385 - if ( desktop_mode_is_chromeless_request() ) {
488 +function openstation_chromeless_suppress_admin_bar() {
489 + if ( openstation_is_chromeless_request() ) {
386 490 remove_action( 'in_admin_header', 'wp_admin_bar_render', 0 );
387 491 remove_action( 'wp_body_open', 'wp_admin_bar_render', 0 );
388 492 }
389 493 }
390 -add_action( 'admin_init', 'desktop_mode_chromeless_suppress_admin_bar' );
494 +add_action( 'admin_init', 'openstation_chromeless_suppress_admin_bar' );
391 495
392 496 /**
393 - * Preserves the `desktop_mode_chromeless` flag through admin
497 + * Stops a window from BUILDING the admin bar it never draws.
498 + *
499 + * Removing the render above stops the markup. It does not stop the
500 + * work: `_wp_admin_bar_init()` is hooked on `admin_init`,
501 + * `is_admin_bar_showing()` short-circuits to true for any admin
502 + * request, and so every window still instantiates `WP_Admin_Bar`,
503 + * calls `initialize()`, and — the expensive part — calls
504 + * `add_menus()`, which fires `admin_bar_menu` and runs **every**
505 + * registered callback. Core's twenty-odd nodes, WooCommerce's,
506 + * Jetpack's, a host masterbar's: each one resolving links, counting
507 + * things, checking capabilities. The finished object is then dropped
508 + * on the floor, because nothing renders it.
509 + *
510 + * The shell draws a real admin bar, once. A window drawing none
511 + * should pay for none — this is the same asymmetry the asset trims
512 + * exploit, on the server side.
513 + *
514 + * **Swapping the class rather than unhooking the init** is the
515 + * careful way to do it. `remove_action( 'admin_init',
516 + * '_wp_admin_bar_init' )` would leave `$wp_admin_bar` null, and a
517 + * plugin that touches the global outside the `admin_bar_menu` hook —
518 + * bad practice, entirely real — would fatal on it. Core exposes
519 + * `wp_admin_bar_class` precisely for this, so a window gets a real
520 + * `WP_Admin_Bar` subclass that is fully functional in every respect
521 + * except that it never solicits nodes. `add_node()` still works,
522 + * `get_nodes()` still answers, the global is still an object; the
523 + * hook simply never fires.
524 + *
525 + * `initialize()` is deliberately left alone — it sets up the object's
526 + * own state and costs nothing worth reclaiming.
527 + *
528 + * @param string $class_name Admin bar class WordPress intends to instantiate.
529 + * @return string The silent subclass inside a window; `$class_name` untouched
530 + * everywhere else, and whenever the parent class is unavailable.
531 + */
532 +function openstation_chromeless_silence_admin_bar( $class_name ) {
533 + if ( ! openstation_is_chromeless_request() ) {
534 + return $class_name;
535 + }
536 +
537 + /**
538 + * Filters whether a window skips building the admin bar.
539 + *
540 + * Return false to let a window construct the bar as WordPress
541 + * normally would — for a plugin that (unusually) relies on
542 + * `admin_bar_menu` firing for a side effect rather than for the
543 + * node it adds.
544 + *
545 + * @param bool $silence Defaults to true inside windows.
546 + */
547 + if ( ! apply_filters( 'openstation_chromeless_silence_admin_bar', true ) ) {
548 + return $class_name;
549 + }
550 +
551 + // `_wp_admin_bar_init()` requires `class-wp-admin-bar.php` before
552 + // it applies this filter, so the parent is guaranteed loaded here
553 + // — and only here, which is why the subclass is required lazily
554 + // rather than at bootstrap.
555 + if ( ! class_exists( 'WP_Admin_Bar' ) ) {
556 + return $class_name;
557 + }
558 + require_once __DIR__ . '/class-openstation-silent-admin-bar.php';
559 +
560 + return 'OpenStation_Silent_Admin_Bar';
561 +}
562 +add_filter( 'wp_admin_bar_class', 'openstation_chromeless_silence_admin_bar' );
563 +
564 +/**
565 + * Detaches core's update / maintenance nags inside chromeless iframes so
566 + * they don't repeat in every window — the shell surfaces the update once
567 + * instead.
568 + */
569 +function openstation_chromeless_suppress_update_nags() {
570 + if ( ! openstation_is_chromeless_request() ) {
571 + return;
572 + }
573 + remove_action( 'admin_notices', 'update_nag', 3 );
574 + remove_action( 'network_admin_notices', 'update_nag', 3 );
575 + remove_action( 'admin_notices', 'maintenance_nag', 10 );
576 + remove_action( 'network_admin_notices', 'maintenance_nag', 10 );
577 +}
578 +add_action( 'admin_init', 'openstation_chromeless_suppress_update_nags' );
579 +
580 +/**
581 + * Detaches the remaining global core admin notices inside chromeless iframes
582 + * so they don't repeat in every window — the shell re-derives and surfaces
583 + * each once (see `openstation_get_core_notices()`). The update / maintenance
584 + * nags are handled by `openstation_chromeless_suppress_update_nags()`.
585 + */
586 +function openstation_chromeless_suppress_core_notices() {
587 + if ( ! openstation_is_chromeless_request() ) {
588 + return;
589 + }
590 + remove_action( 'admin_notices', 'wp_recovery_mode_nag', 1 );
591 + remove_action( 'admin_notices', 'default_password_nag' );
592 + remove_action( 'admin_notices', 'deactivated_plugins_notice', 5 );
593 + remove_action( 'admin_notices', 'paused_plugins_notice', 5 );
594 + remove_action( 'admin_notices', 'paused_themes_notice', 5 );
595 +}
596 +add_action( 'admin_init', 'openstation_chromeless_suppress_core_notices' );
597 +
598 +/**
599 + * Keeps core's session-expired login modal (`wp-auth-check`) out of
600 + * chromeless iframes so the parent shell owns the single prompt.
601 + *
602 + * Every chromeless iframe runs its own Heartbeat, and by default
603 + * each one loads `wp-auth-check.js` + the `#wp-auth-check-wrap`
604 + * markup. When the session expires, N open windows meant N stacked
605 + * login modals — all asking for the same credentials. Returning
606 + * false from `wp_auth_check_load` here stops the modal assets from
607 + * ever loading inside iframes; the parent shell (a normal admin
608 + * page) keeps its copy and surfaces the one prompt over the whole
609 + * desktop.
610 + *
611 + * Detection is unaffected: the `wp-auth-check` heartbeat response
612 + * field is attached server-side (core hooks `wp_auth_check()` on
613 + * `heartbeat_send` / `heartbeat_nopriv_send`), so the bridge's
614 + * stale-nonce recovery in `chromeless-bridge.php` still sees the
615 + * logged-out → logged-in flip without the modal JS.
616 + *
617 + * @param bool $show Whether to load the authentication check.
618 + * @return bool
619 + */
620 +function openstation_chromeless_suppress_auth_check( $show ) {
621 + if ( openstation_is_chromeless_request() ) {
622 + return false;
623 + }
624 + return $show;
625 +}
626 +add_filter( 'wp_auth_check_load', 'openstation_chromeless_suppress_auth_check' );
627 +
628 +/**
629 + * Preserves the `openstation_chromeless` flag through admin
394 630 * redirects.
395 631 *
396 632 * A chromeless iframe can be navigated away from chromeless mode
397 633 * by any redirect that drops the query string —
@@ -405,30 +641,28 @@
405 641 * Scope is intentionally narrow: only same-site admin URLs are
406 642 * touched, and only when the current request is itself
407 643 * chromeless. Anything else passes through unchanged.
408 644 *
409 - * @since 0.1.0
410 - *
411 645 * @param string $location The redirect URL.
412 - * @return string The redirect URL, with `desktop_mode_chromeless=1` appended when applicable.
646 + * @return string The redirect URL, with `openstation_chromeless=1` appended when applicable.
413 647 */
414 -function desktop_mode_chromeless_preserve_redirect( $location ) {
415 - if ( empty( $location ) || ! desktop_mode_is_chromeless_request() ) {
648 +function openstation_chromeless_preserve_redirect( $location ) {
649 + if ( empty( $location ) || ! openstation_is_chromeless_request() ) {
416 650 return $location;
417 651 }
418 652
419 - if ( ! desktop_mode_is_admin_redirect_target( $location ) ) {
653 + if ( ! openstation_is_admin_redirect_target( $location ) ) {
420 654 return $location;
421 655 }
422 656
423 657 // Don't double-append if the URL already carries the flag.
424 - if ( false !== strpos( $location, 'desktop_mode_chromeless=' ) ) {
658 + if ( false !== strpos( $location, 'openstation_chromeless=' ) ) {
425 659 return $location;
426 660 }
427 661
428 - return add_query_arg( 'desktop_mode_chromeless', '1', $location );
662 + return add_query_arg( 'openstation_chromeless', '1', $location );
429 663 }
430 -add_filter( 'wp_redirect', 'desktop_mode_chromeless_preserve_redirect', 999 );
664 +add_filter( 'wp_redirect', 'openstation_chromeless_preserve_redirect', 999 );
431 665
432 666 /**
433 667 * Preserves the `desktop_mode_classic` flag through admin
434 668 * redirects.
@@ -444,29 +678,27 @@
444 678 * wp-admin targets, only when the current request is itself a
445 679 * classic-override request, and the flag is never appended
446 680 * twice.
447 681 *
448 - * @since 0.4.0
449 - *
450 682 * @param string $location The redirect URL.
451 683 * @return string The redirect URL, with `desktop_mode_classic=1` appended when applicable.
452 684 */
453 -function desktop_mode_classic_preserve_redirect( $location ) {
454 - if ( empty( $location ) || ! desktop_mode_is_classic_request() ) {
685 +function openstation_classic_preserve_redirect( $location ) {
686 + if ( empty( $location ) || ! openstation_is_classic_request() ) {
455 687 return $location;
456 688 }
457 689
458 - if ( ! desktop_mode_is_admin_redirect_target( $location ) ) {
690 + if ( ! openstation_is_admin_redirect_target( $location ) ) {
459 691 return $location;
460 692 }
461 693
462 - if ( false !== strpos( $location, DESKTOP_MODE_CLASSIC_FLAG . '=' ) ) {
694 + if ( false !== strpos( $location, OPENSTATION_CLASSIC_FLAG . '=' ) ) {
463 695 return $location;
464 696 }
465 697
466 - return add_query_arg( DESKTOP_MODE_CLASSIC_FLAG, '1', $location );
698 + return add_query_arg( OPENSTATION_CLASSIC_FLAG, '1', $location );
467 699 }
468 -add_filter( 'wp_redirect', 'desktop_mode_classic_preserve_redirect', 999 );
700 +add_filter( 'wp_redirect', 'openstation_classic_preserve_redirect', 999 );
469 701
470 702 /**
471 703 * Whether `$location` is a redirect target that lands inside
472 704 * wp-admin on the current site. Handles all four shapes WP core
@@ -482,16 +714,14 @@
482 714 * Off-site redirects (login → external SSO, e.g.) and frontend
483 715 * redirects (`/`, `/?p=42`) return false so we never paint our
484 716 * query flag on URLs that don't run our admin code.
485 717 *
486 - * @since 0.8.0
487 - *
488 718 * @internal
489 719 *
490 720 * @param string $location Raw redirect URL handed to `wp_redirect`.
491 721 * @return bool
492 722 */
493 -function desktop_mode_is_admin_redirect_target( $location ) {
723 +function openstation_is_admin_redirect_target( $location ) {
494 724 $location = (string) $location;
495 725 if ( '' === $location ) {
496 726 return false;
497 727 }
@@ -518,9 +748,9 @@
518 748 return true;
519 749 }
520 750 // Absolute path NOT into wp-admin (e.g. `/`, `/wp-login.php`,
521 751 // `/wp-json/...`). Frontend or login flow — leave alone.
522 - if ( '/' === $path[ 0 ] ) {
752 + if ( '/' === $path[0] ) {
523 753 return false;
524 754 }
525 755 }
526 756