PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.8
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 0.8.6 All 33 releases
desktop-mode / includes / shell-screen.php

shell-screen.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.8, at includes/shell-screen.php

615 lines 24.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — the shell screen.
4 *
5 * The desktop shell is served by an admin screen OpenStation owns:
6 * `admin.php?page=openstation`, a menu-hidden page whose only enqueues
7 * are OpenStation's own plus the every-admin-page baseline. The shell
8 * used to be painted OVER whatever admin screen the portal forwarded
9 * to — the Dashboard by default, the last-focused window's URL
10 * otherwise — and so inherited that screen's entire script and style
11 * queue, its server-side render, and its hidden HTML. On a site running
12 * the Gutenberg plugin that meant the whole editor closure printed,
13 * parsed and executed in the shell's realm, where nothing ever rendered
14 * it (162 requests / 20 MB raw on the QA instance, against 32 requests
15 * / 1.8 MB for OpenStation's own assets).
16 *
17 * The portal keeps its URL and its frozen query vars. The target it
18 * already resolves becomes a parameter the shell screen reads instead
19 * of a screen the shell rides on:
20 *
21 * /openstation/?target=…
22 * → admin.php?page=openstation&target=<admin path>&intent=1
23 * /openstation/
24 * → admin.php?page=openstation (screen resolves the entry)
25 * /wp-admin/edit.php (plain admin GET)
26 * → admin.php?page=openstation&target=/wp-admin/edit.php&intent=1
27 * /wp-admin/index.php?desktop_mode_portal=1 (pre-screen bookmark)
28 * → admin.php?page=openstation&target=/wp-admin/index.php
29 *
30 * Why an admin page rather than a standalone document served from
31 * `parse_request`: `is_admin()` must be true and `admin_menu` /
32 * `admin_enqueue_scripts` must fire, because those are the documented
33 * contract behind every `openstation_register_*` call, the menu-payload
34 * harvest, and every plugin that gates its registration on `is_admin()`
35 * at load time. The admin page keeps all of that for free.
36 *
37 * `openstation_is_shell_request()` is the one predicate for "this
38 * request paints the shell". It replaces the implicit "enabled, not
39 * chromeless, not classic" that several render hooks used to spell out
40 * on their own, each meaning "shell" without saying so.
41 *
42 * @package OpenStation
43 */
44
45 defined( 'ABSPATH' ) || exit;
46
47 /**
48 * The `page=` slug of the shell screen.
49 */
50 const OPENSTATION_SHELL_PAGE_SLUG = 'openstation';
51
52 /**
53 * The screen id WordPress assigns to the shell screen —
54 * `get_current_screen()->id` and the `$hook_suffix` passed to
55 * `admin_enqueue_scripts` on a shell boot.
56 *
57 * A submenu page registered under an empty parent gets the `admin_`
58 * prefix, so this is `admin_page_openstation` rather than a
59 * `toplevel_page_*` or `<parent>_page_*` name.
60 */
61 const OPENSTATION_SHELL_SCREEN_ID = 'admin_page_openstation';
62
63 /**
64 * Query arg on the shell screen carrying the admin URL to open first.
65 * Same name and same value shape as the portal's own `target`, so the
66 * two are validated by the same sanitiser.
67 */
68 const OPENSTATION_SHELL_TARGET_ARG = 'target';
69
70 /**
71 * Query arg on the shell screen marking `target` as the user's own
72 * navigation intent (a followed link, a bookmark) rather than a
73 * destination the portal picked. Mirrors the portal's intent flag; the
74 * shell reads it as `fromPortalIntent`.
75 */
76 const OPENSTATION_SHELL_INTENT_ARG = 'intent';
77
78 /**
79 * Query arg asking the shell screen to boot straight into overview: how
80 * a switch from another site's overview lands in this one's, tiles and
81 * all (on a network every site is its own OpenStation, see
82 * docs/multisite.md). One-shot like the two above — read here, handed
83 * to the shell as `landInOverview`, stripped from the address bar.
84 */
85 const OPENSTATION_SHELL_OVERVIEW_ARG = 'openstation_overview';
86
87 /**
88 * Whether this shell-screen request asked to boot into overview.
89 *
90 * @return bool
91 */
92 function openstation_shell_lands_in_overview() {
93 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing flag.
94 return openstation_is_shell_screen_request() && ! empty( $_GET[ OPENSTATION_SHELL_OVERVIEW_ARG ] );
95 }
96
97 /**
98 * The side this shell slides its desk in from, when it was reached by a
99 * switch from another origin: `next`, `prev`, or ''. A one-shot boot
100 * arg like the two above (`openstation_hop_from`), because the hint a
101 * same-origin switch leaves in sessionStorage never crosses origins.
102 *
103 * @return string
104 */
105 function openstation_shell_arrival_direction() {
106 if ( ! openstation_is_shell_screen_request() ) {
107 return '';
108 }
109 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing flag.
110 $from = isset( $_GET[ OPENSTATION_NETWORK_HOP_FROM_ARG ] ) && is_scalar( $_GET[ OPENSTATION_NETWORK_HOP_FROM_ARG ] ) ? sanitize_key( wp_unslash( $_GET[ OPENSTATION_NETWORK_HOP_FROM_ARG ] ) ) : '';
111 return in_array( $from, array( 'next', 'prev' ), true ) ? $from : '';
112 }
113
114 /**
115 * Builds the shell screen URL, optionally carrying a target.
116 *
117 * `$target` is an absolute same-origin admin URL or a request-URI-shaped
118 * path (`/wp-admin/edit.php?post_type=page`). Either way only its path
119 * and query travel: the screen re-validates the value on read through
120 * {@see openstation_sanitize_portal_target()}, so the parameter is
121 * never trusted from the URL alone. An empty target yields the bare
122 * screen URL and the screen resolves the entry itself.
123 *
124 * The screen exists in both admins, so the URL follows the one the
125 * caller is in. `$network` overrides that for callers with no context
126 * of their own: `admin-ajax.php` is never the network admin, whatever
127 * the click that reached it.
128 *
129 * @param string $target Admin URL to open first, or '' for none.
130 * @param bool $intent Whether the target is the user's navigation intent.
131 * @param bool|null $network Force the network screen (true) or the site
132 * one (false). Null follows the request.
133 * @return string Absolute shell screen URL.
134 */
135 function openstation_shell_url( $target = '', $intent = false, $network = null ) {
136 $target = is_string( $target ) ? openstation_shell_normalize_admin_url( $target ) : '';
137 $path = '' !== $target ? wp_parse_url( $target, PHP_URL_PATH ) : '';
138
139 if ( null === $network ) {
140 // Follow the admin the TARGET lives in, falling back to the
141 // request's own. The network dashboard opened on the site
142 // screen would be one admin inside another's shell, which is
143 // what the bridge refuses on a link click for the same reason.
144 $network = is_string( $path ) && '' !== $path
145 ? false !== strpos( $path, '/wp-admin/network/' )
146 : is_network_admin();
147 }
148
149 $screen = 'admin.php?page=' . OPENSTATION_SHELL_PAGE_SLUG;
150 $url = $network ? network_admin_url( $screen ) : admin_url( $screen );
151
152 if ( '' !== $target ) {
153 $query = wp_parse_url( $target, PHP_URL_QUERY );
154 if ( is_string( $path ) && '' !== $path ) {
155 $relative = $path . ( is_string( $query ) && '' !== $query ? '?' . $query : '' );
156 $url = add_query_arg( OPENSTATION_SHELL_TARGET_ARG, rawurlencode( $relative ), $url );
157 if ( $intent ) {
158 $url = add_query_arg( OPENSTATION_SHELL_INTENT_ARG, '1', $url );
159 }
160 }
161 }
162
163 return $url;
164 }
165
166 /**
167 * Rebuilds a URL's query through `http_build_query()`, so every value
168 * is percent-encoded exactly once.
169 *
170 * The portal sanitiser hands back a URL whose query values are decoded
171 * (`plugin=dir/file.php`): `add_query_arg()` re-encodes what was already
172 * in a query string but not the args it is given. WordPress's own links
173 * spell that value `dir%2Ffile.php`, and the shell used to build
174 * `currentPage` with `http_build_query( $_GET )`, which does too. Both
175 * the redirect the screen is reached by and the page it opens go through
176 * here, so the same URL reads the same on every hop.
177 *
178 * @param string $url URL, absolute or request-URI-shaped.
179 * @return string The URL with a normalised query; '' for a non-string.
180 */
181 function openstation_shell_normalize_admin_url( $url ) {
182 if ( ! is_string( $url ) || '' === $url ) {
183 return '';
184 }
185 $query = wp_parse_url( $url, PHP_URL_QUERY );
186 if ( ! is_string( $query ) || '' === $query ) {
187 return $url;
188 }
189 parse_str( $query, $args );
190 $base = substr( $url, 0, (int) strpos( $url, '?' ) );
191 $hash = wp_parse_url( $url, PHP_URL_FRAGMENT );
192
193 return $base
194 . ( ! empty( $args ) ? '?' . http_build_query( $args ) : '' )
195 . ( is_string( $hash ) && '' !== $hash ? '#' . $hash : '' );
196 }
197
198 /**
199 * Whether `$url` addresses the shell screen.
200 *
201 * Accepts absolute URLs and request-URI-shaped paths. Used wherever a
202 * URL is about to become a window or a redirect target: the shell must
203 * never open itself inside a window, and a saved session or a `target`
204 * pointing at the screen must fall back rather than loop.
205 *
206 * @param string $url URL or path to test.
207 * @return bool
208 */
209 function openstation_url_is_shell_screen( $url ) {
210 if ( ! is_string( $url ) || '' === $url ) {
211 return false;
212 }
213 $path = wp_parse_url( $url, PHP_URL_PATH );
214 $query = wp_parse_url( $url, PHP_URL_QUERY );
215 if ( ! is_string( $path ) || ! is_string( $query ) ) {
216 return false;
217 }
218 if ( 'admin.php' !== basename( $path ) ) {
219 return false;
220 }
221 parse_str( $query, $args );
222 return isset( $args['page'] ) && OPENSTATION_SHELL_PAGE_SLUG === $args['page'];
223 }
224
225 /**
226 * Whether the current request is for the shell screen.
227 *
228 * Reads the current screen once it exists. Before `set_current_screen()`
229 * — on `admin_init`, where the portal redirect runs — the screen is
230 * not there yet, so the `$plugin_page` global (populated from `?page=`
231 * by `admin.php` before `admin_menu`) is the early answer.
232 *
233 * Says nothing about whether the shell renders here: a disabled user, a
234 * chromeless load or a classic-flagged request can all address this
235 * screen. {@see openstation_is_shell_request()} is that answer.
236 *
237 * @return bool
238 */
239 function openstation_is_shell_screen_request() {
240 if ( ! is_admin() ) {
241 return false;
242 }
243 if ( function_exists( 'get_current_screen' ) ) {
244 $screen = get_current_screen();
245 if ( $screen instanceof WP_Screen ) {
246 // WordPress suffixes screen ids in the network admin, so
247 // the network shell is `admin_page_openstation-network`.
248 return in_array(
249 $screen->id,
250 array( OPENSTATION_SHELL_SCREEN_ID, OPENSTATION_SHELL_SCREEN_ID . '-network' ),
251 true
252 );
253 }
254 }
255 global $pagenow, $plugin_page;
256 return 'admin.php' === $pagenow
257 && isset( $plugin_page )
258 && OPENSTATION_SHELL_PAGE_SLUG === $plugin_page;
259 }
260
261 /**
262 * Whether the current request paints the desktop shell.
263 *
264 * True on the shell screen for a user with OpenStation enabled, and on
265 * a solo request (`?openstation_solo=<id>`, the native host's
266 * one-window boot, which renders in place wherever it lands). Never
267 * inside a window (chromeless) and never on a classic-flagged request.
268 *
269 * Every hook that used to gate on "enabled, not chromeless, not
270 * classic" reads this instead: the shell markup, its assets, the
271 * `os-active` body class, native-window templates, the palette
272 * deferral, the PWA head tags, desktop-theme styles.
273 *
274 * @return bool
275 */
276 function openstation_is_shell_request() {
277 if ( ! is_admin() ) {
278 return false;
279 }
280 if ( ! openstation_is_enabled() ) {
281 return false;
282 }
283 if ( openstation_is_chromeless_request() || openstation_is_classic_request() ) {
284 return false;
285 }
286 if ( openstation_is_shell_screen_request() ) {
287 return true;
288 }
289 return function_exists( 'openstation_is_solo_request' ) && openstation_is_solo_request();
290 }
291
292 /**
293 * Registers the shell screen.
294 *
295 * An empty parent slug keeps the page out of the menu: WordPress only
296 * paints submenus of entries that exist in `$menu`, so `$submenu['']`
297 * is registered, routable and highlighted nowhere. The `read`
298 * capability is the same floor the portal applies, so every user who
299 * can enter the desktop can reach its screen.
300 */
301 function openstation_register_shell_screen() {
302 $hook = add_submenu_page(
303 '',
304 __( 'OpenStation', 'desktop-mode' ),
305 __( 'OpenStation', 'desktop-mode' ),
306 'read',
307 OPENSTATION_SHELL_PAGE_SLUG,
308 'openstation_render_shell_screen'
309 );
310
311 if ( $hook ) {
312 add_action( "load-{$hook}", 'openstation_shell_screen_set_title' );
313 }
314 }
315 add_action( 'admin_menu', 'openstation_register_shell_screen' );
316 // The network admin builds its menu from its own hook, and the desktop
317 // is reachable there for the same reason it is on a site: it is where
318 // the network's own admin pages are. `network/admin.php` routes
319 // `?page=` exactly as `admin.php` does.
320 add_action( 'network_admin_menu', 'openstation_register_shell_screen' );
321
322 /**
323 * Names the shell document, before `admin-header.php` asks for a name.
324 *
325 * `get_admin_page_title()` finds no title for a page whose parent is the
326 * empty menu — it walks `$menu` and `$submenu` for an entry that paints,
327 * and this screen deliberately has none. So the global `$title` stayed
328 * null, and `admin-header.php` line 41 runs `strip_tags( $title )` on it
329 * unconditionally.
330 *
331 * On PHP 8.1+ that is a deprecation notice, and with `WP_DEBUG_DISPLAY`
332 * on it PRINTS — before `<!DOCTYPE html>`, because the header has not
333 * emitted it yet. A document whose first bytes are not the doctype loads
334 * in QUIRKS MODE, and quirks mode is not a cosmetic difference here: the
335 * quirks UA stylesheet stops `<table>` inheriting `color` and `font-*`
336 * from its ancestors. Every `<os-table>` in a native window therefore
337 * dropped the palette's `--os-ui-fg` and fell back to core's
338 * `body { color: #3c434a }` — near-black text on the station's dark
339 * surfaces, at 1.3:1 against a table header (#697 → the Pages window).
340 *
341 * `load-{$hook}` fires in `admin.php` before `admin-header.php` is
342 * required, so a real string is in place by the time core reads it.
343 * `get_admin_page_title()` then returns early on its own `! empty()`
344 * check, which is also what gives the document the word before the
345 * chevron: "OpenStation ‹ Site — WordPress".
346 *
347 * Set unconditionally: the screen wants its name whether or not the
348 * shell paints on this request ({@see openstation_render_shell_screen()}
349 * answers with a pointer at the portal when it does not).
350 */
351 function openstation_shell_screen_set_title() {
352 $GLOBALS['title'] = __( 'OpenStation', 'desktop-mode' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- naming an admin screen IS writing $title; every core screen does it (options-general.php, edit.php), and admin-header.php reads it moments later.
353 }
354
355 /**
356 * The shell screen's page callback.
357 *
358 * Prints nothing when the shell renders: the markup goes out from
359 * `in_admin_header @ 5` ({@see openstation_render_shell()}), the same
360 * hook as always, so it lands before the notices and the admin bar
361 * rather than after them — moving it here would change stacking and
362 * the timing of the `os-active` body class. The callback only speaks
363 * when the screen is reached without the shell: a user with
364 * OpenStation off, or a classic-flagged request. Then it points at the
365 * portal, which is the opt-in surface.
366 */
367 function openstation_render_shell_screen() {
368 if ( openstation_is_shell_request() ) {
369 return;
370 }
371 ?>
372 <div class="wrap">
373 <h1><?php esc_html_e( 'OpenStation', 'desktop-mode' ); ?></h1>
374 <p>
375 <?php esc_html_e( 'OpenStation is not active for your account on this request.', 'desktop-mode' ); ?>
376 <a href="<?php echo esc_url( openstation_portal_url() ); ?>"><?php esc_html_e( 'Open the desktop', 'desktop-mode' ); ?></a>
377 </p>
378 </div>
379 <?php
380 }
381
382 /**
383 * Resolves what the shell boots with on this request.
384 *
385 * On the shell screen the boot page comes from the `target` query arg
386 * — validated through the portal's sanitiser and refused when it names
387 * the shell screen itself — and falls back to
388 * {@see openstation_portal_entry_url()} exactly as the portal used to:
389 * the session's focused window, else the default window, else the
390 * Dashboard. `fromPortal` is true by construction there (the screen is
391 * only ever reached through a redirect), and `fromPortalIntent` is the
392 * `intent` arg, honoured only when the target was valid.
393 *
394 * Off the screen — a solo boot rendering in place — the page is the
395 * request's own URL, built from `$pagenow` and `$_GET` with the frozen
396 * portal flags stripped so the derived window id matches the dock's.
397 *
398 * @return array {
399 * @type string $url Absolute admin URL the shell opens first.
400 * @type bool $fromPortal Whether the shell was reached through a redirect.
401 * @type bool $fromPortalIntent Whether `url` is the user's own navigation intent.
402 * }
403 */
404 function openstation_shell_boot_target() {
405 if ( openstation_is_shell_screen_request() ) {
406 $target = '';
407 $intent = false;
408 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing arg, validated below.
409 if ( ! empty( $_GET[ OPENSTATION_SHELL_TARGET_ARG ] ) && is_scalar( $_GET[ OPENSTATION_SHELL_TARGET_ARG ] ) ) {
410 // `esc_url_raw`, not `sanitize_text_field`, for the reason
411 // recorded on the portal handler: the latter strips every
412 // percent-encoded sequence and mangles `plugin=dir%2Ffile.php`.
413 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing arg, validated below.
414 $target = openstation_sanitize_portal_target( esc_url_raw( wp_unslash( $_GET[ OPENSTATION_SHELL_TARGET_ARG ] ) ) );
415 if ( '' !== $target ) {
416 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing arg.
417 $intent = ! empty( $_GET[ OPENSTATION_SHELL_INTENT_ARG ] );
418 }
419 }
420 if ( '' === $target ) {
421 // The network screen has its own dashboard to fall back to;
422 // the saved session belongs to a site, so it would open one
423 // admin's window on the other's desktop.
424 $target = is_network_admin()
425 ? network_admin_url( 'index.php' )
426 : openstation_portal_entry_url( get_current_user_id() );
427 }
428 $target = openstation_shell_normalize_admin_url( $target );
429
430 // An admin URL alone names the directory; `$pagenow` on that
431 // request is `index.php`, and the dock derives the Dashboard's
432 // window id from the file. Keep both sides deriving the same id,
433 // naming the file inside the target's OWN admin so a network
434 // URL does not resolve to the site's dashboard.
435 $path = wp_parse_url( $target, PHP_URL_PATH );
436 if ( is_string( $path ) && '/' === substr( $path, -1 ) ) {
437 $query = wp_parse_url( $target, PHP_URL_QUERY );
438 $parts = explode( '?', $target, 2 );
439 $target = rtrim( $parts[0], '/' ) . '/index.php'
440 . ( is_string( $query ) && '' !== $query ? '?' . $query : '' );
441 }
442
443 return array(
444 'url' => $target,
445 'fromPortal' => true,
446 'fromPortalIntent' => $intent,
447 );
448 }
449
450 global $pagenow;
451 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only, rebuilds the request's own URL.
452 $query = $_GET;
453 unset( $query[ OPENSTATION_PORTAL_FLAG ], $query[ OPENSTATION_PORTAL_INTENT_FLAG ] );
454
455 return array(
456 'url' => admin_url( (string) $pagenow ) . ( ! empty( $query ) ? '?' . http_build_query( $query ) : '' ),
457 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request flag.
458 'fromPortal' => ! empty( $_GET[ OPENSTATION_PORTAL_FLAG ] ),
459 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request flag.
460 'fromPortalIntent' => ! empty( $_GET[ OPENSTATION_PORTAL_INTENT_FLAG ] ),
461 );
462 }
463
464 /**
465 * Finds the dock entry — a top-level item or one of its submenu
466 * children — whose URL is the boot page, so the entry window opens
467 * with that entry's title and icon rather than the screen's own.
468 *
469 * On the shell screen `$title` is "OpenStation" and `$parent_file` is
470 * empty, which used to be the host screen's title and menu icon: the
471 * first window would flash "OpenStation" until the iframe reported its
472 * own title. Matching against the dock is the same identity the shell
473 * uses to fold the entry window into its tile.
474 *
475 * @param string $url Absolute admin URL the shell opens first.
476 * @param array $dock_items Dock payload from `openstation_build_dock_items()`.
477 * @return array{title:string,icon:string} Empty strings when nothing matches.
478 */
479 function openstation_shell_boot_target_meta( $url, $dock_items ) {
480 $none = array(
481 'title' => '',
482 'icon' => '',
483 );
484 if ( ! is_string( $url ) || '' === $url || ! is_array( $dock_items ) ) {
485 return $none;
486 }
487 $key = openstation_shell_url_match_key( $url );
488 if ( '' === $key ) {
489 return $none;
490 }
491 foreach ( $dock_items as $item ) {
492 if ( ! is_array( $item ) ) {
493 continue;
494 }
495 $icon = isset( $item['icon'] ) && is_string( $item['icon'] ) ? $item['icon'] : '';
496 if ( isset( $item['url'] ) && openstation_shell_url_match_key( $item['url'] ) === $key ) {
497 return array(
498 'title' => isset( $item['title'] ) ? (string) $item['title'] : '',
499 'icon' => $icon,
500 );
501 }
502 if ( empty( $item['submenu'] ) || ! is_array( $item['submenu'] ) ) {
503 continue;
504 }
505 foreach ( $item['submenu'] as $sub ) {
506 if ( is_array( $sub ) && isset( $sub['url'] ) && openstation_shell_url_match_key( $sub['url'] ) === $key ) {
507 return array(
508 'title' => isset( $sub['title'] ) ? (string) $sub['title'] : '',
509 'icon' => $icon,
510 );
511 }
512 }
513 }
514 return $none;
515 }
516
517 /**
518 * Comparable key for two admin URLs: path plus sorted query, with the
519 * chromeless and portal flags dropped — the PHP twin of the shell's
520 * `urlMatchKey()`.
521 *
522 * @param string $url URL to key.
523 * @return string '' when the URL has no path.
524 */
525 function openstation_shell_url_match_key( $url ) {
526 if ( ! is_string( $url ) ) {
527 return '';
528 }
529 $path = wp_parse_url( $url, PHP_URL_PATH );
530 if ( ! is_string( $path ) || '' === $path ) {
531 return '';
532 }
533 $query = wp_parse_url( $url, PHP_URL_QUERY );
534 $args = array();
535 if ( is_string( $query ) && '' !== $query ) {
536 parse_str( $query, $args );
537 unset( $args['openstation_chromeless'], $args[ OPENSTATION_PORTAL_FLAG ], $args[ OPENSTATION_PORTAL_INTENT_FLAG ] );
538 ksort( $args );
539 }
540 return rtrim( $path, '/' ) . '?' . http_build_query( $args );
541 }
542
543 /**
544 * Drops operator-named handles from the shell screen's queues.
545 *
546 * With no host screen, what still prints on the shell is OpenStation's
547 * own assets, Core's every-admin-page set, and whatever plugins enqueue
548 * on every admin page — a global nag, a tracker, a chat bubble. The
549 * framework does not guess which of those "belongs" in the shell; the
550 * site says so, through `openstation_shell_dequeue_handles`.
551 *
552 * Runs at `PHP_INT_MAX` so every plugin has enqueued, and only on a
553 * shell boot: windows keep the chromeless trims, classic pages keep
554 * everything. A named handle that a surviving script or style still
555 * depends on is refused with a `_doing_it_wrong()` rather than dropped,
556 * the same closure rule the chromeless trim applies — dequeuing it
557 * would strand the dependent. Dequeue, never deregister: a handle that
558 * stays registered can still be resolved as a dependency.
559 */
560 function openstation_shell_dequeue_assets() {
561 if ( ! openstation_is_shell_request() || ! openstation_is_shell_screen_request() ) {
562 return;
563 }
564
565 foreach ( array( 'script', 'style' ) as $kind ) {
566 /**
567 * Filters the handles dequeued from the shell screen.
568 *
569 * Called once for scripts and once for styles. Default empty:
570 * the shell removes nothing it did not put there unless told
571 * to. A handle a surviving asset depends on is refused.
572 *
573 * @param string[] $handles Handles to dequeue. Default empty.
574 * @param string $kind `script` or `style`.
575 */
576 $handles = apply_filters( 'openstation_shell_dequeue_handles', array(), $kind );
577 $handles = array_values( array_unique( array_filter( (array) $handles, 'is_string' ) ) );
578 if ( empty( $handles ) ) {
579 continue;
580 }
581
582 $registry = 'script' === $kind ? wp_scripts() : wp_styles();
583 if ( ! $registry ) {
584 continue;
585 }
586
587 $drops = array_values( array_intersect( $handles, (array) $registry->queue ) );
588 if ( empty( $drops ) ) {
589 continue;
590 }
591 $safe = openstation_protect_survivor_dependencies( $registry, $registry->queue, $drops );
592 $refused = array_diff( $drops, $safe );
593 foreach ( $refused as $handle ) {
594 _doing_it_wrong(
595 __FUNCTION__,
596 sprintf(
597 /* translators: 1: script or style handle, 2: script or style */
598 esc_html__( 'The %2$s handle "%1$s" cannot leave the shell screen: something still enqueued depends on it.', 'desktop-mode' ),
599 esc_html( $handle ),
600 esc_html( $kind )
601 ),
602 ''
603 );
604 }
605 foreach ( $safe as $handle ) {
606 if ( 'script' === $kind ) {
607 wp_dequeue_script( $handle );
608 } else {
609 wp_dequeue_style( $handle );
610 }
611 }
612 }
613 }
614 add_action( 'admin_enqueue_scripts', 'openstation_shell_dequeue_assets', PHP_INT_MAX );
615