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/render/chromeless-bridge.php +136 -4 1.1.5 → 1.1.13 View file →
@@ -209,9 +209,26 @@
209 209 * it runs after every other admin script — a second or more after the
210 210 * browser painted the "Settings saved." notice the ring is
211 211 * confirming. From the head it beats the body to the screen.
212 212 *
213 - * The parent ignores it unless that window has a submit waiting.
213 + * It also says where the frame landed, for documents the shell cannot
214 + * read: one sent with `Document-Isolation-Policy`, as WordPress sends
215 + * the block editor in Chromium, is cross-origin to the parent, and a
216 + * Revisions window that restores into the editor still has to hand off.
217 + *
218 + * The parent acts on it only for a window with a submit waiting, or
219 + * one whose frame it cannot read.
220 + *
221 + * A document can also change its address without navigating, and the
222 + * block editor does on its first save: `post-new.php` becomes
223 + * `post.php?post=N&action=edit` through `history.replaceState()`. For
224 + * a frame the parent cannot read, nothing else tells it, so the window
225 + * would go on calling the draft "a new post", and "Copy link" would
226 + * hand out an empty editor. `os-iframe-location` reports every such
227 + * change, and only updates where the parent thinks the frame is: it is
228 + * not a navigation, so it settles no submit and resets no title. The
229 + * wrap is installed here, ahead of every other script, so Core's own
230 + * canonical-URL `replaceState()` in this same head is reported too.
214 231 */
215 232 function openstation_chromeless_navigation_ping_script() {
216 233 if ( ! openstation_is_chromeless_request() ) {
217 234 return;
@@ -217,9 +234,20 @@
217 234 return;
218 235 }
219 236
220 237 wp_print_inline_script_tag(
221 - "try{if(window.parent&&window.parent!==window){window.parent.postMessage({type:'os-iframe-navigated'},window.location.origin);}}catch(e){}"
238 + 'try{if(window.parent&&window.parent!==window){'
239 + . "window.parent.postMessage({type:'os-iframe-navigated',url:window.location.href},window.location.origin);"
240 + . 'var osParent=window.parent,osOrigin=window.location.origin,'
241 + . "osMoved=function(){try{osParent.postMessage({type:'os-iframe-location',url:window.location.href},osOrigin);}catch(e){}};"
242 + . "['pushState','replaceState'].forEach(function(name){"
243 + . 'var original=window.history[name];'
244 + . "if(typeof original!=='function'){return;}"
245 + . 'window.history[name]=function(){var result=original.apply(this,arguments);'
246 + . 'osMoved();return result;};'
247 + . '});'
248 + . "window.addEventListener('popstate',osMoved);"
249 + . '}}catch(e){}'
222 250 );
223 251 }
224 252 add_action( 'admin_head', 'openstation_chromeless_navigation_ping_script', 1 );
225 253
@@ -327,11 +355,13 @@
327 355 // `admin.php` calls `set_current_screen()` AFTER `admin_init`,
328 356 // so at probe time there is no screen yet — and Core's own
329 357 // enqueue callbacks (the block-editor script loader among
330 358 // them) read `get_current_screen()->id` unguarded. Build the
331 - // screen the real flow would have had before the hook fires.
359 + // screen the real flow would have had before the hook fires,
360 + // in the request's own admin context — see
361 + // `openstation_menu_refresh_probe_screen_id()`.
332 362 if ( function_exists( 'set_current_screen' ) && function_exists( 'get_current_screen' ) && ! get_current_screen() ) {
333 - set_current_screen( 'admin' );
363 + set_current_screen( openstation_menu_refresh_probe_screen_id() );
334 364 }
335 365 ob_start();
336 366 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- deliberate replay of Core's own hook so handle-attached script data exists before the harvest below.
337 367 do_action( 'admin_enqueue_scripts', 'admin.php' );
@@ -341,8 +371,35 @@
341 371 return openstation_build_menu_payload();
342 372 }
343 373
344 374 /**
375 + * The screen id the refresh probe builds its placeholder `WP_Screen`
376 + * from: `admin-network` in the network admin, `admin-user` in the user
377 + * admin, `admin` on a site.
378 + *
379 + * Once a screen exists, `is_network_admin()` answers from it — and a
380 + * screen built from a bare id gets its admin context from the id's
381 + * SUFFIX (`-network`, `-user`, or none for a site screen), never from
382 + * `WP_NETWORK_ADMIN`. A plain `admin` screen therefore turned a
383 + * network-admin probe into a site request for everything after it:
384 + * `self_admin_url()` resolved every network menu slug against the site
385 + * admin, and a live refresh on the network shell painted a dock whose
386 + * tiles opened site pages. Read the context here, BEFORE any screen
387 + * exists, where the answer still comes from the request itself.
388 + *
389 + * @return string Screen id carrying the request's admin context.
390 + */
391 +function openstation_menu_refresh_probe_screen_id() {
392 + if ( is_network_admin() ) {
393 + return 'admin-network';
394 + }
395 + if ( is_user_admin() ) {
396 + return 'admin-user';
397 + }
398 + return 'admin';
399 +}
400 +
401 +/**
345 402 * Outputs the chromeless screen-meta bridge script.
346 403 *
347 404 * Detects Screen Options / Help panels in the iframed page and relays
348 405 * their availability + open/closed state to the parent desktop shell
@@ -514,4 +571,79 @@
514 571 wp_enqueue_script( 'os-chromeless-bridge' );
515 572 wp_add_inline_script( 'os-chromeless-bridge', $data, 'before' );
516 573 }
517 574 add_action( 'admin_footer', 'openstation_chromeless_bridge_script' );
575 +
576 +/**
577 + * Transient holding the screen a plugin redirected a plugins window to.
578 + *
579 + * @param int $user_id User ID.
580 + * @return string Transient name.
581 + */
582 +function openstation_plugins_handoff_key( $user_id ) {
583 + return 'openstation_plugins_handoff_' . (int) $user_id;
584 +}
585 +
586 +/**
587 + * Keeps a plugins window on the plugins screen when a plugin redirects
588 + * away from it (Elementor sends a fresh install to its onboarding), and
589 + * leaves the destination for that screen to open in a window of its
590 + * own, the way a link to it would. One hop only, so a plugin that
591 + * redirects on every load cannot loop.
592 + *
593 + * @param string $location Redirect URL.
594 + * @return string The redirect URL, or the plugins screen to land on.
595 + */
596 +function openstation_chromeless_hand_off_plugins_redirect( $location ) {
597 + if ( empty( $location ) || 'plugins.php' !== ( $GLOBALS['pagenow'] ?? '' ) ) {
598 + return $location;
599 + }
600 + if ( ! openstation_is_chromeless_request() || ! openstation_is_admin_redirect_target( $location ) ) {
601 + return $location;
602 + }
603 + $destination = WP_Http::make_absolute_url( $location, self_admin_url() );
604 + if ( 'plugins.php' === basename( (string) wp_parse_url( $destination, PHP_URL_PATH ) ) ) {
605 + return $location;
606 + }
607 + $key = openstation_plugins_handoff_key( get_current_user_id() );
608 + if ( false !== get_transient( $key ) ) {
609 + delete_transient( $key );
610 + return $location;
611 + }
612 + set_transient( $key, $destination, MINUTE_IN_SECONDS );
613 +
614 + // A redirect from inside an action request (a plugin's own
615 + // `activated_plugin` hook) must not run that action again.
616 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only asks whether this is an action request.
617 + if ( empty( $_REQUEST['action'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
618 + return esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
619 + }
620 + return self_admin_url( 'plugins.php' );
621 +}
622 +add_filter( 'wp_redirect', 'openstation_chromeless_hand_off_plugins_redirect', 998 );
623 +
624 +/**
625 + * Opens the screen {@see openstation_chromeless_hand_off_plugins_redirect()}
626 + * kept back, posted as the bridge posts a `target="_blank"` admin link.
627 + */
628 +function openstation_chromeless_open_handed_off_redirect() {
629 + if ( 'plugins.php' !== ( $GLOBALS['pagenow'] ?? '' ) || ! openstation_is_chromeless_request() ) {
630 + return;
631 + }
632 + $key = openstation_plugins_handoff_key( get_current_user_id() );
633 + $destination = get_transient( $key );
634 + if ( ! is_string( $destination ) || '' === $destination ) {
635 + return;
636 + }
637 + delete_transient( $key );
638 +
639 + $message = wp_json_encode(
640 + array(
641 + 'type' => 'os-iframe-admin-link',
642 + 'url' => $destination,
643 + 'label' => '',
644 + 'newContext' => true,
645 + )
646 + );
647 + wp_print_inline_script_tag( 'try{window.parent.postMessage(' . $message . ',window.location.origin);}catch(e){}' );
648 +}
649 +add_action( 'admin_footer', 'openstation_chromeless_open_handed_off_redirect' );