PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.13
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.13
1.1.13 1.1.12 1.1.11 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 All 37 releases
← All changes | includes/core/routing.php +287 -107 0.9.7 → 1.1.13 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_url_is_same_admin()} — same-origin admin URL predicate
19 - * - {@see desktop_mode_resolve_admin_target()} — admin filename → URL resolver
20 - * - {@see desktop_mode_admin_target_allowlist()} — wp-admin filename allowlist
21 - * - {@see desktop_mode_is_chromeless_request()} — chromeless request detection
22 - * - {@see desktop_mode_is_classic_request()} — classic-override request detection
23 - * - {@see desktop_mode_chromeless_hide_admin_bar()} — `show_admin_bar` filter
24 - * - {@see desktop_mode_chromeless_suppress_admin_bar()} — `admin_init` action
25 - * - {@see desktop_mode_chromeless_preserve_redirect()} — `wp_redirect` filter
26 - * - {@see desktop_mode_classic_preserve_redirect()} — `wp_redirect` filter
27 - * - {@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
28 30 *
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.8.1
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.8.1
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.8.1
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,82 @@
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 + * Whether the request came from a classic-override page. A navigation
429 + * a page starts from script (`location.href = …`, as "Edit with
430 + * Elementor" does) carries no flag, but its Referer still does.
431 + *
432 + * @return bool
433 + */
434 +function openstation_is_classic_referer() {
435 + if ( empty( $_SERVER['HTTP_REFERER'] ) ) {
436 + return false;
437 + }
438 + $referer = esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) );
439 + $query = wp_parse_url( $referer, PHP_URL_QUERY );
440 + if ( ! openstation_url_is_same_admin( $referer ) || ! is_string( $query ) ) {
441 + return false;
442 + }
443 + parse_str( $query, $args );
444 + return isset( $args[ OPENSTATION_CLASSIC_FLAG ] ) && '1' === $args[ OPENSTATION_CLASSIC_FLAG ];
445 +}
446 +
447 +/**
448 + * Checks whether the browser is fetching this request as a
449 + * sub-resource of some page rather than navigating to it.
450 + *
451 + * Admin URLs serve more than pages. Jetpack's admin-bar sparkline is
452 + * an `<img>` whose src is `admin.php?page=stats&noheader&proxy&chart=…`:
453 + * core's `admin.php` skips the header on `noheader` and the page hook
454 + * echoes PNG bytes. The Jetpack Stats screen loads its report body the
455 + * same way, over XHR. Treating such a request as "a user landing on a
456 + * plain admin page" and forwarding it into the desktop hands the
457 + * consumer an HTML document instead: the admin bar then draws a broken
458 + * image with the alt text where the chart should be.
459 + *
460 + * `Sec-Fetch-Mode` is the browser's own answer, set by the user agent
461 + * and immune to script. `navigate` is a document or frame load, the
462 + * only kind of request worth forwarding into the desktop; `cors`,
463 + * `no-cors`, `same-origin` and `websocket` are sub-resource fetches.
464 + * A missing header (an old browser, a proxy that strips it) answers
465 + * false: not known to be a sub-resource, so callers keep behaving as
466 + * they always did.
467 + *
468 + * @return bool True when the request is a sub-resource fetch.
469 + */
470 +function openstation_is_subresource_request() {
471 + if ( empty( $_SERVER['HTTP_SEC_FETCH_MODE'] ) ) {
472 + return false;
473 + }
474 + $mode = strtolower( sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_FETCH_MODE'] ) ) );
475 + return '' !== $mode && 'navigate' !== $mode;
476 +}
477 +
478 +/**
351 479 * Disables the admin bar on chromeless (iframe) requests.
352 480 *
353 481 * Hooked on the `show_admin_bar` filter so the front-end bar path
354 482 * also sees a false return. In admin, `is_admin_bar_showing()`
@@ -353,23 +481,21 @@
353 481 * Hooked on the `show_admin_bar` filter so the front-end bar path
354 482 * also sees a false return. In admin, `is_admin_bar_showing()`
355 483 * short-circuits to true for any `is_admin()` request regardless
356 484 * of this filter, so the actual render is stopped by
357 - * {@see desktop_mode_chromeless_suppress_admin_bar()} below; this
485 + * {@see openstation_chromeless_suppress_admin_bar()} below; this
358 486 * filter is kept for completeness + tests.
359 487 *
360 - * @since 0.1.0
361 - *
362 488 * @param bool $show Whether the admin bar should be shown.
363 489 * @return bool
364 490 */
365 -function desktop_mode_chromeless_hide_admin_bar( $show ) {
366 - if ( desktop_mode_is_chromeless_request() ) {
491 +function openstation_chromeless_hide_admin_bar( $show ) {
492 + if ( openstation_is_chromeless_request() ) {
367 493 return false;
368 494 }
369 495 return $show;
370 496 }
371 -add_filter( 'show_admin_bar', 'desktop_mode_chromeless_hide_admin_bar' );
497 +add_filter( 'show_admin_bar', 'openstation_chromeless_hide_admin_bar' );
372 498
373 499 /**
374 500 * Suppresses the admin bar render inside chromeless iframes.
375 501 *
@@ -377,28 +503,92 @@
377 503 * context, so the `show_admin_bar` filter alone can't stop
378 504 * `wp_admin_bar_render()` from firing on `in_admin_header`. We
379 505 * detach the render action instead and let chromeless.css hide
380 506 * the `wp-toolbar` padding on `<html>`.
381 - *
382 - * @since 0.1.0
383 507 */
384 -function desktop_mode_chromeless_suppress_admin_bar() {
385 - if ( desktop_mode_is_chromeless_request() ) {
508 +function openstation_chromeless_suppress_admin_bar() {
509 + if ( openstation_is_chromeless_request() ) {
386 510 remove_action( 'in_admin_header', 'wp_admin_bar_render', 0 );
387 511 remove_action( 'wp_body_open', 'wp_admin_bar_render', 0 );
388 512 }
389 513 }
390 -add_action( 'admin_init', 'desktop_mode_chromeless_suppress_admin_bar' );
514 +add_action( 'admin_init', 'openstation_chromeless_suppress_admin_bar' );
391 515
392 516 /**
517 + * Stops a window from BUILDING the admin bar it never draws.
518 + *
519 + * Removing the render above stops the markup. It does not stop the
520 + * work: `_wp_admin_bar_init()` is hooked on `admin_init`,
521 + * `is_admin_bar_showing()` short-circuits to true for any admin
522 + * request, and so every window still instantiates `WP_Admin_Bar`,
523 + * calls `initialize()`, and — the expensive part — calls
524 + * `add_menus()`, which fires `admin_bar_menu` and runs **every**
525 + * registered callback. Core's twenty-odd nodes, WooCommerce's,
526 + * Jetpack's, a host masterbar's: each one resolving links, counting
527 + * things, checking capabilities. The finished object is then dropped
528 + * on the floor, because nothing renders it.
529 + *
530 + * The shell draws a real admin bar, once. A window drawing none
531 + * should pay for none — this is the same asymmetry the asset trims
532 + * exploit, on the server side.
533 + *
534 + * **Swapping the class rather than unhooking the init** is the
535 + * careful way to do it. `remove_action( 'admin_init',
536 + * '_wp_admin_bar_init' )` would leave `$wp_admin_bar` null, and a
537 + * plugin that touches the global outside the `admin_bar_menu` hook —
538 + * bad practice, entirely real — would fatal on it. Core exposes
539 + * `wp_admin_bar_class` precisely for this, so a window gets a real
540 + * `WP_Admin_Bar` subclass that is fully functional in every respect
541 + * except that it never solicits nodes. `add_node()` still works,
542 + * `get_nodes()` still answers, the global is still an object; the
543 + * hook simply never fires.
544 + *
545 + * `initialize()` is deliberately left alone — it sets up the object's
546 + * own state and costs nothing worth reclaiming.
547 + *
548 + * @param string $class_name Admin bar class WordPress intends to instantiate.
549 + * @return string The silent subclass inside a window; `$class_name` untouched
550 + * everywhere else, and whenever the parent class is unavailable.
551 + */
552 +function openstation_chromeless_silence_admin_bar( $class_name ) {
553 + if ( ! openstation_is_chromeless_request() ) {
554 + return $class_name;
555 + }
556 +
557 + /**
558 + * Filters whether a window skips building the admin bar.
559 + *
560 + * Return false to let a window construct the bar as WordPress
561 + * normally would — for a plugin that (unusually) relies on
562 + * `admin_bar_menu` firing for a side effect rather than for the
563 + * node it adds.
564 + *
565 + * @param bool $silence Defaults to true inside windows.
566 + */
567 + if ( ! apply_filters( 'openstation_chromeless_silence_admin_bar', true ) ) {
568 + return $class_name;
569 + }
570 +
571 + // `_wp_admin_bar_init()` requires `class-wp-admin-bar.php` before
572 + // it applies this filter, so the parent is guaranteed loaded here
573 + // — and only here, which is why the subclass is required lazily
574 + // rather than at bootstrap.
575 + if ( ! class_exists( 'WP_Admin_Bar' ) ) {
576 + return $class_name;
577 + }
578 + require_once __DIR__ . '/class-openstation-silent-admin-bar.php';
579 +
580 + return 'OpenStation_Silent_Admin_Bar';
581 +}
582 +add_filter( 'wp_admin_bar_class', 'openstation_chromeless_silence_admin_bar' );
583 +
584 +/**
393 585 * Detaches core's update / maintenance nags inside chromeless iframes so
394 586 * they don't repeat in every window — the shell surfaces the update once
395 587 * instead.
396 - *
397 - * @since 0.9.4
398 588 */
399 -function desktop_mode_chromeless_suppress_update_nags() {
400 - if ( ! desktop_mode_is_chromeless_request() ) {
589 +function openstation_chromeless_suppress_update_nags() {
590 + if ( ! openstation_is_chromeless_request() ) {
401 591 return;
402 592 }
403 593 remove_action( 'admin_notices', 'update_nag', 3 );
404 594 remove_action( 'network_admin_notices', 'update_nag', 3 );
@@ -404,20 +594,18 @@
404 594 remove_action( 'network_admin_notices', 'update_nag', 3 );
405 595 remove_action( 'admin_notices', 'maintenance_nag', 10 );
406 596 remove_action( 'network_admin_notices', 'maintenance_nag', 10 );
407 597 }
408 -add_action( 'admin_init', 'desktop_mode_chromeless_suppress_update_nags' );
598 +add_action( 'admin_init', 'openstation_chromeless_suppress_update_nags' );
409 599
410 600 /**
411 601 * Detaches the remaining global core admin notices inside chromeless iframes
412 602 * so they don't repeat in every window — the shell re-derives and surfaces
413 - * each once (see `desktop_mode_get_core_notices()`). The update / maintenance
414 - * nags are handled by `desktop_mode_chromeless_suppress_update_nags()`.
415 - *
416 - * @since 0.9.6
603 + * each once (see `openstation_get_core_notices()`). The update / maintenance
604 + * nags are handled by `openstation_chromeless_suppress_update_nags()`.
417 605 */
418 -function desktop_mode_chromeless_suppress_core_notices() {
419 - if ( ! desktop_mode_is_chromeless_request() ) {
606 +function openstation_chromeless_suppress_core_notices() {
607 + if ( ! openstation_is_chromeless_request() ) {
420 608 return;
421 609 }
422 610 remove_action( 'admin_notices', 'wp_recovery_mode_nag', 1 );
423 611 remove_action( 'admin_notices', 'default_password_nag' );
@@ -424,9 +612,9 @@
424 612 remove_action( 'admin_notices', 'deactivated_plugins_notice', 5 );
425 613 remove_action( 'admin_notices', 'paused_plugins_notice', 5 );
426 614 remove_action( 'admin_notices', 'paused_themes_notice', 5 );
427 615 }
428 -add_action( 'admin_init', 'desktop_mode_chromeless_suppress_core_notices' );
616 +add_action( 'admin_init', 'openstation_chromeless_suppress_core_notices' );
429 617
430 618 /**
431 619 * Keeps core's session-expired login modal (`wp-auth-check`) out of
432 620 * chromeless iframes so the parent shell owns the single prompt.
@@ -445,23 +633,21 @@
445 633 * `heartbeat_send` / `heartbeat_nopriv_send`), so the bridge's
446 634 * stale-nonce recovery in `chromeless-bridge.php` still sees the
447 635 * logged-out → logged-in flip without the modal JS.
448 636 *
449 - * @since 0.9.8
450 - *
451 637 * @param bool $show Whether to load the authentication check.
452 638 * @return bool
453 639 */
454 -function desktop_mode_chromeless_suppress_auth_check( $show ) {
455 - if ( desktop_mode_is_chromeless_request() ) {
640 +function openstation_chromeless_suppress_auth_check( $show ) {
641 + if ( openstation_is_chromeless_request() ) {
456 642 return false;
457 643 }
458 644 return $show;
459 645 }
460 -add_filter( 'wp_auth_check_load', 'desktop_mode_chromeless_suppress_auth_check' );
646 +add_filter( 'wp_auth_check_load', 'openstation_chromeless_suppress_auth_check' );
461 647
462 648 /**
463 - * Preserves the `desktop_mode_chromeless` flag through admin
649 + * Preserves the `openstation_chromeless` flag through admin
464 650 * redirects.
465 651 *
466 652 * A chromeless iframe can be navigated away from chromeless mode
467 653 * by any redirect that drops the query string —
@@ -475,30 +661,28 @@
475 661 * Scope is intentionally narrow: only same-site admin URLs are
476 662 * touched, and only when the current request is itself
477 663 * chromeless. Anything else passes through unchanged.
478 664 *
479 - * @since 0.1.0
480 - *
481 665 * @param string $location The redirect URL.
482 - * @return string The redirect URL, with `desktop_mode_chromeless=1` appended when applicable.
666 + * @return string The redirect URL, with `openstation_chromeless=1` appended when applicable.
483 667 */
484 -function desktop_mode_chromeless_preserve_redirect( $location ) {
485 - if ( empty( $location ) || ! desktop_mode_is_chromeless_request() ) {
668 +function openstation_chromeless_preserve_redirect( $location ) {
669 + if ( empty( $location ) || ! openstation_is_chromeless_request() ) {
486 670 return $location;
487 671 }
488 672
489 - if ( ! desktop_mode_is_admin_redirect_target( $location ) ) {
673 + if ( ! openstation_is_admin_redirect_target( $location ) ) {
490 674 return $location;
491 675 }
492 676
493 677 // Don't double-append if the URL already carries the flag.
494 - if ( false !== strpos( $location, 'desktop_mode_chromeless=' ) ) {
678 + if ( false !== strpos( $location, 'openstation_chromeless=' ) ) {
495 679 return $location;
496 680 }
497 681
498 - return add_query_arg( 'desktop_mode_chromeless', '1', $location );
682 + return add_query_arg( 'openstation_chromeless', '1', $location );
499 683 }
500 -add_filter( 'wp_redirect', 'desktop_mode_chromeless_preserve_redirect', 999 );
684 +add_filter( 'wp_redirect', 'openstation_chromeless_preserve_redirect', 999 );
501 685
502 686 /**
503 687 * Preserves the `desktop_mode_classic` flag through admin
504 688 * redirects.
@@ -514,29 +698,27 @@
514 698 * wp-admin targets, only when the current request is itself a
515 699 * classic-override request, and the flag is never appended
516 700 * twice.
517 701 *
518 - * @since 0.4.0
519 - *
520 702 * @param string $location The redirect URL.
521 703 * @return string The redirect URL, with `desktop_mode_classic=1` appended when applicable.
522 704 */
523 -function desktop_mode_classic_preserve_redirect( $location ) {
524 - if ( empty( $location ) || ! desktop_mode_is_classic_request() ) {
705 +function openstation_classic_preserve_redirect( $location ) {
706 + if ( empty( $location ) || ! openstation_is_classic_request() ) {
525 707 return $location;
526 708 }
527 709
528 - if ( ! desktop_mode_is_admin_redirect_target( $location ) ) {
710 + if ( ! openstation_is_admin_redirect_target( $location ) ) {
529 711 return $location;
530 712 }
531 713
532 - if ( false !== strpos( $location, DESKTOP_MODE_CLASSIC_FLAG . '=' ) ) {
714 + if ( false !== strpos( $location, OPENSTATION_CLASSIC_FLAG . '=' ) ) {
533 715 return $location;
534 716 }
535 717
536 - return add_query_arg( DESKTOP_MODE_CLASSIC_FLAG, '1', $location );
718 + return add_query_arg( OPENSTATION_CLASSIC_FLAG, '1', $location );
537 719 }
538 -add_filter( 'wp_redirect', 'desktop_mode_classic_preserve_redirect', 999 );
720 +add_filter( 'wp_redirect', 'openstation_classic_preserve_redirect', 999 );
539 721
540 722 /**
541 723 * Whether `$location` is a redirect target that lands inside
542 724 * wp-admin on the current site. Handles all four shapes WP core
@@ -552,16 +734,14 @@
552 734 * Off-site redirects (login → external SSO, e.g.) and frontend
553 735 * redirects (`/`, `/?p=42`) return false so we never paint our
554 736 * query flag on URLs that don't run our admin code.
555 737 *
556 - * @since 0.8.0
557 - *
558 738 * @internal
559 739 *
560 740 * @param string $location Raw redirect URL handed to `wp_redirect`.
561 741 * @return bool
562 742 */
563 -function desktop_mode_is_admin_redirect_target( $location ) {
743 +function openstation_is_admin_redirect_target( $location ) {
564 744 $location = (string) $location;
565 745 if ( '' === $location ) {
566 746 return false;
567 747 }
@@ -588,9 +768,9 @@
588 768 return true;
589 769 }
590 770 // Absolute path NOT into wp-admin (e.g. `/`, `/wp-login.php`,
591 771 // `/wp-json/...`). Frontend or login flow — leave alone.
592 - if ( '/' === $path[ 0 ] ) {
772 + if ( '/' === $path[0] ) {
593 773 return false;
594 774 }
595 775 }
596 776