| @@ -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 | |
| @@ -543,4 +571,79 @@ | ||
| 543 | 571 | wp_enqueue_script( 'os-chromeless-bridge' ); |
| 544 | 572 | wp_add_inline_script( 'os-chromeless-bridge', $data, 'before' ); |
| 545 | 573 | } |
| 546 | 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' ); | |