PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.7
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
desktop-mode / includes / shell-screen.php

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

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