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/render/shell.php +46 -295 0.8.71.1.10 View file →
@@ -1,18 +1,18 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Shell markup injection.
3 + * OpenStation — Shell markup injection.
4 4 *
5 - * Emits the `<div id="desktop-mode-shell">…</div>` skeleton at
6 - * `in_admin_header @ 5`. The shell floats on top of the classic
7 - * admin via `position: fixed`; the body class added by
8 - * `body-classes.php` triggers the CSS that hides classic chrome.
5 + * Emits the `<div id="os-shell">…</div>` skeleton at
6 + * `in_admin_header @ 5` on the shell screen (`includes/shell-screen.php`).
7 + * The shell floats on top of the admin document via `position: fixed`;
8 + * the body class added by `body-classes.php` triggers the CSS that
9 + * hides classic chrome.
9 10 *
10 11 * Extracted from `render.php` during the architecture-0.8.1 PHP
11 12 * slicing (phase 6).
12 13 *
13 - * @package Desktop_Mode
14 - * @since 0.8.1
14 + * @package OpenStation
15 15 */
16 16
17 17 defined( 'ABSPATH' ) || exit;
18 18
@@ -22,24 +22,20 @@
22 22 *
23 23 * Runs on `in_admin_header` at priority 5 so the shell renders right
24 24 * after the classic admin bar but before the page content. The shell
25 25 * floats above the classic layout via `position: fixed` in CSS; the
26 - * classic sidebar, body, and footer are hidden with `body.desktop-mode-active`
26 + * classic sidebar, body, and footer are hidden with `body.os-active`
27 27 * selectors.
28 - *
29 - * @since 0.1.0
30 28 */
31 -function desktop_mode_render_shell() {
32 - if ( desktop_mode_is_chromeless_request() || ! desktop_mode_is_enabled() || desktop_mode_is_classic_request() ) {
29 +function openstation_render_shell() {
30 + if ( ! openstation_is_shell_request() ) {
33 31 return;
34 32 }
35 33
36 34 /**
37 35 * Fires right before the desktop shell markup is rendered.
38 - *
39 - * @since 0.1.0
40 36 */
41 - do_action( 'desktop_mode_shell_before' );
37 + do_action( 'openstation_shell_before' );
42 38
43 39 // Stamp the user's admin color scheme onto the shell root so the
44 40 // variables.css per-scheme selectors kick in before first paint —
45 41 // doing this from JS on init() would show the default palette for a
@@ -44,10 +40,26 @@
44 40 // variables.css per-scheme selectors kick in before first paint —
45 41 // doing this from JS on init() would show the default palette for a
46 42 // frame before swapping.
47 43 $scheme = sanitize_html_class( get_user_option( 'admin_color' ), 'fresh' );
44 +
45 + // Same reasoning for the active desktop theme: the compiled
46 + // theme stylesheet keys off this attribute, so stamping it
47 + // server-side means the first paint is already themed. Setting
48 + // it from `applyDesktopTheme()` on boot would flash the default
49 + // palette for a frame. Empty string when the user is on the
50 + // system default (and nothing else about themes runs at all).
51 + $desktop_theme = function_exists( 'openstation_active_desktop_theme_slug' )
52 + ? openstation_active_desktop_theme_slug()
53 + : '';
54 +
55 + // A shell asked to boot into overview (a switch from another site's
56 + // switcher) arrives with its desk hidden until overview is up, so the
57 + // desk slides in as one piece instead of the bare desk flashing first.
58 + // See `src/multisite/instance-transition.ts`.
59 + $arriving = openstation_shell_lands_in_overview() ? ' os-shell--arriving' : '';
48 60 ?>
49 - <div id="desktop-mode-shell" class="desktop-mode-shell" data-desktop-mode-scheme="<?php echo esc_attr( $scheme ); ?>" role="application" aria-label="<?php esc_attr_e( 'Desktop shell', 'desktop-mode' ); ?>">
61 + <div id="os-shell" class="os-shell<?php echo esc_attr( $arriving ); ?>" data-os-scheme="<?php echo esc_attr( $scheme ); ?>"<?php echo '' !== $desktop_theme ? ' data-os-desktop-theme="' . esc_attr( $desktop_theme ) . '"' : ''; ?> role="application" aria-label="<?php esc_attr_e( 'Desktop shell', 'desktop-mode' ); ?>">
50 62 <?php
51 63 /*
52 64 * Wallpaper layer — sits behind both the dock and the desktop
53 65 * area so a translucent dock bleeds through to the wallpaper
@@ -52,21 +64,30 @@
52 64 * Wallpaper layer — sits behind both the dock and the desktop
53 65 * area so a translucent dock bleeds through to the wallpaper
54 66 * (macOS pattern). Canvas-driven wallpapers mount their own
55 67 * DOM into this element; static CSS wallpapers just inherit
56 - * the `--desktop-mode-bg` custom property the shell sets at
68 + * the `--os-bg` custom property the shell sets at
57 69 * boot. Presentational only.
58 70 */
59 71 ?>
60 - <div id="desktop-mode-wallpaper" class="desktop-mode-wallpaper" aria-hidden="true"></div>
61 - <div class="desktop-mode-shell__body">
62 - <nav id="desktop-mode-dock" class="desktop-mode-dock" role="toolbar" aria-label="<?php esc_attr_e( 'Admin navigation', 'desktop-mode' ); ?>"></nav>
63 - <div id="desktop-mode-area" class="desktop-mode-area desktop-mode-area--with-dock desktop-mode-area--booting">
72 + <div id="os-wallpaper" class="os-wallpaper" aria-hidden="true"></div>
73 + <div class="os-shell__body">
74 + <?php
75 + /*
76 + * `data-os-dock-behavior` is stamped here, not only by the
77 + * shell's apply pass, so a `dynamic` dock is folded from the
78 + * first paint instead of flashing on screen and folding once
79 + * the JS lands. The Split layout's sidebar is synthesised by
80 + * JS and stamped there.
81 + */
82 + ?>
83 + <nav id="os-dock" class="os-dock" role="toolbar" aria-label="<?php esc_attr_e( 'Admin navigation', 'desktop-mode' ); ?>" data-os-dock-behavior="<?php echo esc_attr( openstation_get_dock_behavior() ); ?>"></nav>
84 + <div id="os-area" class="os-area os-area--with-dock os-area--booting">
64 85 <?php
65 86 /*
66 87 * Widget column — paints above the wallpaper but
67 88 * beneath windows (z-index 1 vs. windows at 100+).
68 - * Hosted INSIDE `.desktop-mode-area` so scrolling the
89 + * Hosted INSIDE `.os-area` so scrolling the
69 90 * area (not that we do today) would scroll widgets
70 91 * with it, and so the dock naturally frames
71 92 * it. Empty on first render — JS (`WidgetLayer`)
72 93 * populates it on boot.
@@ -71,9 +92,9 @@
71 92 * it. Empty on first render — JS (`WidgetLayer`)
72 93 * populates it on boot.
73 94 */
74 95 ?>
75 - <aside id="desktop-mode-widgets" class="desktop-mode-widgets" aria-label="<?php esc_attr_e( 'Widgets', 'desktop-mode' ); ?>"></aside>
96 + <aside id="os-widgets" class="os-widgets" aria-label="<?php esc_attr_e( 'Widgets', 'desktop-mode' ); ?>"></aside>
76 97 </div>
77 98 </div>
78 99 </div>
79 100 <?php
@@ -78,279 +99,9 @@
78 99 </div>
79 100 <?php
80 101 /**
81 102 * Fires right after the desktop shell markup has rendered.
82 - *
83 - * @since 0.1.0
84 103 */
85 - do_action( 'desktop_mode_shell_after' );
104 + do_action( 'openstation_shell_after' );
86 105 }
87 -add_action( 'in_admin_header', 'desktop_mode_render_shell', 5 );
106 +add_action( 'in_admin_header', 'openstation_render_shell', 5 );
88 107
89 -/**
90 - * Parent-shell counterpart to the chromeless bridge's stale-nonce
91 - * recovery (see `chromeless-bridge.php`).
92 - *
93 - * Core's `wp-auth-check.js` shows `#wp-auth-check-wrap` (the dark
94 - * backdrop + login iframe) when a heartbeat tick returns
95 - * `wp-auth-check: false`. It only dismisses the overlay when the
96 - * user re-authenticates inside *its own* sub-iframe — re-auth
97 - * happening anywhere else (a chromeless iframe inside our shell,
98 - * another browser tab, the classic admin in another window) leaves
99 - * the parent shell stuck behind an orphaned backdrop.
100 - *
101 - * Beyond the backdrop, the bigger problem is that **WordPress
102 - * nonces are tied to the user's session token**, and re-auth mints
103 - * a fresh token. Every nonce the parent shell cached at page load
104 - * (`wp.desktop.config.restNonce`, plus whatever third-party
105 - * registries/widgets pulled in) was generated against the old
106 - * token and is now silently rejected by `wp_verify_nonce()` /
107 - * `check_ajax_referer()` — even though the auth cookie itself is
108 - * valid. WP reports that as "Cookie check failed", which is
109 - * misleading; the cookie is the only thing still working.
110 - *
111 - * Fix: on `wp-auth-check: false → true`, do a hard reload of the
112 - * parent. The chromeless iframes already self-reload via their
113 - * own bridge-side handler, but the parent is the only place where
114 - * stale shell-wide nonces live, and there is no in-place API to
115 - * swap every cached nonce across every loaded bundle + every
116 - * plugin. The session-saver's `pagehide` flush writes the latest
117 - * window snapshot before unload, so window positions / open
118 - * windows are preserved across the reload.
119 - *
120 - * @since 0.18.5
121 - */
122 -function desktop_mode_parent_auth_check_recovery_script() {
123 - if (
124 - desktop_mode_is_chromeless_request()
125 - || ! desktop_mode_is_enabled()
126 - || desktop_mode_is_classic_request()
127 - ) {
128 - return;
129 - }
130 - $js = <<<'JS'
131 -//# sourceURL=desktop-mode-parent-auth-recovery.js
132 -( function () {
133 - var sawLoggedOut = false;
134 -
135 - /* -----------------------------------------------------------------
136 - * Fast-path auth-check: on 401/403 from any same-origin admin
137 - * request, force `wp.heartbeat.connectNow()` instead of waiting
138 - * up to 60s for the next regular tick. Same logic ships
139 - * inside chromeless iframes via the bridge — this is the
140 - * parent-shell counterpart for the shell's own fetches
141 - * (session-save, REST registries, etc.).
142 - *
143 - * Debounced (5s) so a burst of failed requests doesn't fire a
144 - * storm of heartbeats. URL gate skips heartbeat itself and
145 - * wp-login.php so the recovery can't loop on the very request
146 - * the modal authenticates with.
147 - * ----------------------------------------------------------------- */
148 - var authCooldownUntil = 0;
149 - function maybeForceAuthCheck( status, url ) {
150 - if ( status !== 401 && status !== 403 ) {
151 - return;
152 - }
153 - try {
154 - var resolved = new URL( String( url || '' ), window.location.href );
155 - if ( resolved.origin !== window.location.origin ) {
156 - return;
157 - }
158 - if (
159 - resolved.pathname.indexOf( '/wp-admin/admin-ajax.php' ) !== -1
160 - && /(?:^|&|\?)action=heartbeat(?:&|$)/.test( resolved.search )
161 - ) {
162 - return;
163 - }
164 - if ( resolved.pathname.indexOf( '/wp-login.php' ) !== -1 ) {
165 - return;
166 - }
167 - } catch ( _err ) {
168 - return;
169 - }
170 - var now = Date.now();
171 - if ( now < authCooldownUntil ) {
172 - return;
173 - }
174 - authCooldownUntil = now + 5000;
175 - try {
176 - if (
177 - window.wp
178 - && window.wp.heartbeat
179 - && typeof window.wp.heartbeat.connectNow === 'function'
180 - ) {
181 - window.wp.heartbeat.connectNow();
182 - }
183 - } catch ( _err ) { /* swallow */ }
184 - }
185 -
186 - if ( typeof window.fetch === 'function' ) {
187 - var origFetch = window.fetch;
188 - window.fetch = function ( input, init ) {
189 - var url = '';
190 - if ( typeof input === 'string' ) {
191 - url = input;
192 - } else if ( input && typeof input === 'object' ) {
193 - url = input.url || '';
194 - }
195 - var p;
196 - try {
197 - p = origFetch.apply( this, arguments );
198 - } catch ( sync ) {
199 - throw sync;
200 - }
201 - return p.then( function ( res ) {
202 - try { maybeForceAuthCheck( res.status, url ); } catch ( _e ) {}
203 - return res;
204 - } );
205 - };
206 - }
207 - if ( typeof XMLHttpRequest !== 'undefined' ) {
208 - var origOpen = XMLHttpRequest.prototype.open;
209 - XMLHttpRequest.prototype.open = function ( method, url ) {
210 - try { this.__wpdAuthUrl = url; } catch ( _e ) {}
211 - return origOpen.apply( this, arguments );
212 - };
213 - var origSend = XMLHttpRequest.prototype.send;
214 - XMLHttpRequest.prototype.send = function () {
215 - var xhr = this;
216 - try {
217 - xhr.addEventListener( 'loadend', function () {
218 - try { maybeForceAuthCheck( xhr.status, xhr.__wpdAuthUrl ); } catch ( _e ) {}
219 - } );
220 - } catch ( _e ) {}
221 - return origSend.apply( this, arguments );
222 - };
223 - }
224 -
225 - function recoverFromReauth() {
226 - // Strip the overlay first so the user sees the shell come
227 - // back to life *before* the reload starts, instead of
228 - // looking at a frozen dark backdrop while the network
229 - // stalls. The reload guarantees nonces refresh.
230 - try {
231 - var wrap = document.getElementById( 'wp-auth-check-wrap' );
232 - if ( wrap && wrap.parentNode ) {
233 - wrap.parentNode.removeChild( wrap );
234 - }
235 - document.documentElement.classList.remove( 'wp-auth-check-show' );
236 - document.body.classList.remove( 'modal-open' );
237 - } catch ( _err ) { /* DOM gone — nothing useful to do */ }
238 -
239 - // Reload every open iframe BEFORE the parent reload. Two
240 - // reasons:
241 - //
242 - // 1. Each iframe is also showing core's wp-auth-check
243 - // modal (each one runs its own heartbeat). Without
244 - // this, those modals linger until each iframe's own
245 - // next heartbeat tick (up to 60s) — visible as a
246 - // "frozen iframe with a login modal" while the rest of
247 - // the shell is interactive again.
248 - //
249 - // 2. If an iframe was bounced to `wp-login.php` because it
250 - // made a server request while logged-out, the
251 - // session-saver may have captured that URL. The parent
252 - // reload would restore the iframe AT wp-login.php
253 - // instead of at the original admin page. Telling each
254 - // iframe to `location.reload()` directly makes the
255 - // browser walk its history back through the login
256 - // bounce now that cookies are fresh — the iframe lands
257 - // on the page it was originally on.
258 - //
259 - // Same-origin only (cross-origin iframes wouldn't be ours
260 - // anyway).
261 - try {
262 - var frames = document.querySelectorAll( 'iframe' );
263 - for ( var i = 0; i < frames.length; i++ ) {
264 - try {
265 - // Cross-origin access throws — caught + ignored.
266 - var fw = frames[ i ].contentWindow;
267 - if ( fw && fw.location && typeof fw.location.reload === 'function' ) {
268 - fw.location.reload();
269 - }
270 - } catch ( _crossOrigin ) { /* not ours */ }
271 - }
272 - } catch ( _err ) { /* swallow */ }
273 -
274 - // Hard reload — the only reliable way to refresh every
275 - // nonce baked into JS globals across every loaded bundle.
276 - // Small delay lets the session-saver's `pagehide` write
277 - // the current window snapshot AND gives the
278 - // `wp-auth-check-iframe` from core a chance to relay the
279 - // success postMessage out (some plugins listen for that).
280 - try {
281 - window.setTimeout( function () {
282 - try {
283 - window.location.reload();
284 - } catch ( _err ) { /* swallow */ }
285 - }, 250 );
286 - } catch ( _err ) {
287 - try { window.location.reload(); } catch ( _e ) {}
288 - }
289 - }
290 - // Cross-iframe nudge. The chromeless bridge inside each iframe
291 - // posts `desktop-mode-reauth-detected` the instant its own
292 - // heartbeat sees `wp-auth-check: false → true`. Without this
293 - // the parent has to wait for ITS heartbeat to tick (15s active,
294 - // up to 60s idle) before recoverFromReauth fires — during which
295 - // every REST call from the shell keeps returning 401 with the
296 - // stale shell-wide nonce. With this, the parent's recovery
297 - // starts within a frame of the iframe seeing the new cookie.
298 - try {
299 - window.addEventListener( 'message', function ( ev ) {
300 - if ( ev.origin !== window.location.origin ) {
301 - return;
302 - }
303 - if ( ! ev.data || typeof ev.data !== 'object' ) {
304 - return;
305 - }
306 - if ( ev.data.type !== 'desktop-mode-reauth-detected' ) {
307 - return;
308 - }
309 - // Recovery is idempotent (the reload-of-everything path
310 - // can only fire once before the page is gone), but
311 - // gate on `sawLoggedOut` anyway so a stray message
312 - // from a misbehaving iframe doesn't reload the shell
313 - // during a normal session.
314 - if ( sawLoggedOut ) {
315 - sawLoggedOut = false;
316 - recoverFromReauth();
317 - } else {
318 - // Even if we never noticed the logout ourselves,
319 - // the iframe did. Trust it and recover — the
320 - // stale-nonce gap is real even when the parent
321 - // dodged the auth-check modal entirely.
322 - recoverFromReauth();
323 - }
324 - } );
325 - } catch ( _err ) { /* swallow */ }
326 -
327 - function attach() {
328 - if ( ! window.jQuery ) {
329 - return false;
330 - }
331 - window.jQuery( document ).on( 'heartbeat-tick.wpdParentAuthRecover', function ( ev, data ) {
332 - if ( ! data || typeof data !== 'object' || ! ( 'wp-auth-check' in data ) ) {
333 - return;
334 - }
335 - if ( data[ 'wp-auth-check' ] === false ) {
336 - sawLoggedOut = true;
337 - return;
338 - }
339 - if ( sawLoggedOut && data[ 'wp-auth-check' ] === true ) {
340 - sawLoggedOut = false;
341 - recoverFromReauth();
342 - }
343 - } );
344 - return true;
345 - }
346 - if ( ! attach() ) {
347 - if ( document.readyState === 'loading' ) {
348 - document.addEventListener( 'DOMContentLoaded', attach, { once: true } );
349 - }
350 - window.addEventListener( 'load', attach, { once: true } );
351 - }
352 -} )();
353 -JS;
354 - wp_print_inline_script_tag( $js );
355 -}
356 -add_action( 'admin_footer', 'desktop_mode_parent_auth_check_recovery_script' );