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/core/payload.php +146 -57 1.1.71.1.10 View file →
@@ -454,11 +454,12 @@
454 454 * URLs) would therefore fill the dock with tiles that can only ever
455 455 * escape to a browser tab, which breaks the shell's navigation model.
456 456 * Those entries are dropped from the payload instead.
457 457 *
458 - * `self_admin_url()`, `admin_url()` and `home_url()` hosts all count as
459 - * ours: a site can run its admin on a different domain than its front
460 - * end, and the network admin lives on the network's own.
458 + * The menu's own admin (`openstation_menu_admin_url()`), `admin_url()`
459 + * and `home_url()` hosts all count as ours: a site can run its admin on
460 + * a different domain than its front end, and the network admin lives on
461 + * the network's own.
461 462 *
462 463 * @param string $url Absolute URL, as returned by `openstation_menu_item_url()`.
463 464 * @return bool True when the URL is off-site.
464 465 */
@@ -467,9 +468,9 @@
467 468 $external = false;
468 469
469 470 if ( $host ) {
470 471 $ours = array();
471 - foreach ( array( self_admin_url(), admin_url(), home_url() ) as $known ) {
472 + foreach ( array( openstation_menu_admin_url(), admin_url(), home_url() ) as $known ) {
472 473 $known_host = wp_parse_url( $known, PHP_URL_HOST );
473 474 if ( $known_host ) {
474 475 $ours[] = strtolower( $known_host );
475 476 }
@@ -1662,8 +1663,15 @@
1662 1663 'url' => network_admin_url( 'update-core.php' ),
1663 1664 );
1664 1665 }
1665 1666
1667 + // The site switcher's rows: on a network, the instances this shell
1668 + // may switch to (`openstation_multisite_payload()`), null elsewhere.
1669 + // The Network app spends a menu refresh after every action that
1670 + // changes them (add, remove, join, leave, sync), so the row above
1671 + // overview's desktop tiles follows the registry without a reload.
1672 + $payload['multisite'] = openstation_multisite_payload();
1673 +
1666 1674 // A cheap structural fingerprint of the admin menu the shell uses to
1667 1675 // decide whether a live refresh is warranted. Shipped in every full
1668 1676 // payload so the shell can seed / update its last-known signature
1669 1677 // without recomputing it client-side (which would risk drift from
@@ -1865,8 +1873,12 @@
1865 1873 if ( $dep_handle === $handle ) {
1866 1874 continue;
1867 1875 }
1868 1876 $payload = openstation_resolve_script_payload( $dep_handle );
1877 + // An alias (no `src`) stays in the list when it carries inline
1878 + // data — that data is the whole reason it was declared, and a
1879 + // plugin's config blob commonly rides one. Nothing to fetch
1880 + // AND nothing to run is the only thing dropped.
1869 1881 if ( '' === $payload['url']
1870 1882 && empty( $payload['before'] )
1871 1883 && empty( $payload['after'] )
1872 1884 && empty( $payload['l10n'] ) ) {
@@ -1912,10 +1924,14 @@
1912 1924 * around the lazy `<script src>` in the same order
1913 1925 * `WP_Scripts::do_item()` would have used.
1914 1926 *
1915 1927 * Returns an empty payload (`array( 'url' => '' )`) when the handle
1916 - * is unregistered or has no source — callers treat that as "no
1917 - * script to load."
1928 + * is unregistered. A registered handle with no source — an alias
1929 + * carrying only inline data — also comes back with an empty `url`,
1930 + * but its `before` / `after` / `l10n` are kept: callers that load a
1931 + * bundle treat an empty `url` as "nothing to fetch", and the
1932 + * dependency walk ({@see openstation_resolve_script_dependencies()})
1933 + * still replays what the alias would have printed.
1918 1934 *
1919 1935 * Shared between `openstation_register_window()` and
1920 1936 * `openstation_register_widget()` (and every other registration that
1921 1937 * relies on lazy script loading in the shell) because all of them
@@ -1943,20 +1959,31 @@
1943 1959 return $empty;
1944 1960 }
1945 1961 $registered = $wp_scripts->registered[ $handle ];
1946 1962 $src = is_string( $registered->src ) ? $registered->src : '';
1947 - if ( '' === $src ) {
1948 - return $empty;
1949 - }
1950 1963
1951 - // Normalize relative paths + attach cache-bust ver.
1952 - $resolved = $src;
1953 - if ( 0 === strpos( $resolved, '/' ) && 0 !== strpos( $resolved, '//' ) ) {
1954 - $resolved = site_url( $resolved );
1964 + // A handle with no `src` is an ALIAS — WordPress's supported way
1965 + // to ship inline-only JavaScript (`wp_register_script( $h, false )`
1966 + // plus `wp_add_inline_script()`), and a common home for a plugin's
1967 + // config blob: registering it as a *dependency* of every bundle is
1968 + // what guarantees the config runs first, whatever the enqueue
1969 + // order. `WP_Scripts::do_item()` prints an alias's localized data
1970 + // and its before/after snippets and returns before the `<script
1971 + // src>` it does not have. The payload mirrors that: `url` stays
1972 + // empty (there is nothing to fetch) and the inline data is kept,
1973 + // so a dependency walk can replay it. Translations are not: Core
1974 + // only prints those for a handle it printed a tag for.
1975 + $resolved = '';
1976 + if ( '' !== $src ) {
1977 + // Normalize relative paths + attach cache-bust ver.
1978 + $resolved = $src;
1979 + if ( 0 === strpos( $resolved, '/' ) && 0 !== strpos( $resolved, '//' ) ) {
1980 + $resolved = site_url( $resolved );
1981 + }
1982 + if ( ! empty( $registered->ver ) ) {
1983 + $resolved = add_query_arg( 'ver', $registered->ver, $resolved );
1984 + }
1955 1985 }
1956 - if ( ! empty( $registered->ver ) ) {
1957 - $resolved = add_query_arg( 'ver', $registered->ver, $resolved );
1958 - }
1959 1986
1960 1987 // Harvest `extra` data the lazy-load path would otherwise drop.
1961 1988 $before = array();
1962 1989 $after = array();
@@ -1991,9 +2018,9 @@
1991 2018 // `wp.i18n.setLocaleData( JSON, 'domain' )` snippet that the print
1992 2019 // pipeline emits before the script body. `print_translations(
1993 2020 // $handle, false )` returns the snippet without echoing.
1994 2021 $translations = '';
1995 - if ( method_exists( $wp_scripts, 'print_translations' ) ) {
2022 + if ( '' !== $resolved && method_exists( $wp_scripts, 'print_translations' ) ) {
1996 2023 $captured = $wp_scripts->print_translations( $handle, false );
1997 2024 if ( is_string( $captured ) ) {
1998 2025 $translations = $captured;
1999 2026 }
@@ -2167,24 +2194,16 @@
2167 2194 $script_probe->done = array();
2168 2195 $script_probe->all_deps( $script_roots );
2169 2196 foreach ( $script_probe->to_do as $handle ) {
2170 2197 $payload = openstation_resolve_script_payload( $handle );
2171 - if ( '' === $payload['url'] ) {
2172 - // Src-less aggregator — keep it only for its inline data.
2173 - $registered = isset( $scripts->registered[ $handle ] ) ? $scripts->registered[ $handle ] : null;
2174 - if ( $registered ) {
2175 - foreach ( array( 'before', 'after' ) as $position ) {
2176 - if ( isset( $registered->extra[ $position ] ) && is_array( $registered->extra[ $position ] ) ) {
2177 - $payload[ $position ] = array_values( array_filter( array_map( 'strval', $registered->extra[ $position ] ) ) );
2178 - }
2179 - }
2180 - if ( ! empty( $registered->extra['data'] ) && is_string( $registered->extra['data'] ) ) {
2181 - $payload['l10n'][] = $registered->extra['data'];
2182 - }
2183 - }
2184 - if ( empty( $payload['before'] ) && empty( $payload['after'] ) && empty( $payload['l10n'] ) ) {
2185 - continue;
2186 - }
2198 + // A src-less aggregator is kept only for its inline data — the
2199 + // resolver harvests that for an alias — and dropped when it
2200 + // carries none.
2201 + if ( '' === $payload['url']
2202 + && empty( $payload['before'] )
2203 + && empty( $payload['after'] )
2204 + && empty( $payload['l10n'] ) ) {
2205 + continue;
2187 2206 }
2188 2207 // Core's `initializeCommandPalette( {…} )` inline embeds the
2189 2208 // serialized admin-menu command list — ~20 KB that the boot
2190 2209 // page ALREADY carries as `window.__openStationMenuCommands`
@@ -2358,8 +2377,12 @@
2358 2377 * harvested `wp_localize_script` / `wp_add_inline_script` /
2359 2378 * translations, see `openstation_resolve_script_payload()` — lives
2360 2379 * ONCE per handle in `scriptData`, and the shell joins the two on
2361 2380 * receipt (`hydrateServerEntries()` in `src/native-windows.ts`).
2381 + * Each loadable handle's entry also names its dependency closure in
2382 + * `deps` (ordered handles, every one of them a key of the same map)
2383 + * so the lazy loader can bring a bundle's declared packages — and
2384 + * a src-less alias carrying its config — into the tab before it.
2362 2385 *
2363 2386 * The split exists because script data is a property of the HANDLE,
2364 2387 * not of the window: every App Framework window rides
2365 2388 * `openstation-app-runtime`, and inlining each entry's resolved copy
@@ -2376,9 +2399,9 @@
2376 2399 * Style data stays inline on the entries — it never had a
2377 2400 * duplication problem worth a second map ( companion styles across
2378 2401 * the whole registry total ~2 KB ).
2379 2402 *
2380 - * @return array{windows:array[],scriptData:array<string,array{url:string,before:string[],after:string[],l10n:string[],translations:string}>}
2403 + * @return array{windows:array[],scriptData:array<string,array{url:string,before:string[],after:string[],l10n:string[],translations:string,deps:string[]}>}
2381 2404 */
2382 2405 function openstation_collect_native_windows_payload() {
2383 2406 $empty = array(
2384 2407 'windows' => array(),
@@ -2387,44 +2410,82 @@
2387 2410 if ( ! function_exists( 'openstation_native_window_registry' ) ) {
2388 2411 return $empty;
2389 2412 }
2390 2413
2391 - // Every native window is site-scoped, reading the current site's
2392 - // REST API, so in the network admin a `users.php` tile meaning
2393 - // "everyone on the network" would open one site's user list.
2394 - //
2395 - // This is also what disarms the client-side URL remaps there: they
2396 - // match on the tail of a pathname (`endsWith( '/users.php' )`) and
2397 - // the network admin serves same-named files one directory down, but
2398 - // with nothing registered `openById()` finds no window and the
2399 - // remap falls through to the iframe.
2400 - if ( is_network_admin() ) {
2401 - return $empty;
2402 - }
2403 -
2404 2414 $registry = openstation_native_window_registry();
2405 2415 if ( ! is_array( $registry ) ) {
2406 2416 return $empty;
2407 2417 }
2408 2418
2419 + // A window says which admin offers it (`admin` in its registration:
2420 + // `site`, `network` or `any`). Every native window OpenStation
2421 + // ships is site-scoped, reading the current site's REST API, so in
2422 + // the network admin a `users.php` tile meaning "everyone on the
2423 + // network" would open one site's user list; those stay off the
2424 + // network shell. A window that declares `network` (the Network app)
2425 + // is offered there and nowhere else.
2426 + //
2427 + // Dropping the site windows there is also what disarms the
2428 + // client-side URL remaps: they match on the tail of a pathname
2429 + // (`endsWith( '/users.php' )`) and the network admin serves
2430 + // same-named files one directory down, but with nothing registered
2431 + // `openById()` finds no window and the remap falls through to the
2432 + // iframe.
2433 + $registry = array_filter( $registry, 'openstation_native_window_offered_here' );
2434 +
2409 2435 $script_data = array();
2410 2436
2437 + // Handles resolved as a bundle to LOAD (a window's script, a
2438 + // companion, a tab) and what that visit answered — the handle, or
2439 + // '' for nothing to load — as opposed to reached only as
2440 + // somebody's dependency. A handle can be both — resolved as a
2441 + // dependency first, then named as a window's own script — and
2442 + // only the bundle visit computes its own closure.
2443 + $resolved_as_bundle = array();
2444 +
2411 2445 // Resolve a handle into the map, once. Returns the handle when it
2412 2446 // resolved to something loadable, '' when it did not (never
2413 2447 // registered, no src) — the same silent drop the inline shape
2414 2448 // applied to companions and tab scripts.
2415 - $collect_handle = static function ( $handle ) use ( &$script_data ) {
2449 + //
2450 + // The handle's dependency closure rides along as `deps`: an
2451 + // ordered handle list, each of which lands in the same map. A
2452 + // bundle delivered lazily never goes through WordPress's own
2453 + // dependency resolution — the loader injects one URL — so a
2454 + // window declaring `wp-api-fetch` found `wp.apiFetch` undefined,
2455 + // and one whose config rides a src-less alias handle (a common
2456 + // shape: `wp_register_script( $h, false )` plus
2457 + // `wp_add_inline_script()`, declared as the bundle's dependency
2458 + // so it always runs first) booted with no config at all. Anything
2459 + // the document already ran is skipped on the client, so a page
2460 + // that carried the packages anyway pays nothing.
2461 + $collect_handle = static function ( $handle ) use ( &$script_data, &$resolved_as_bundle ) {
2416 2462 $handle = (string) $handle;
2417 2463 if ( '' === $handle ) {
2418 2464 return '';
2419 2465 }
2420 - if ( isset( $script_data[ $handle ] ) ) {
2421 - return $handle;
2466 + if ( isset( $resolved_as_bundle[ $handle ] ) ) {
2467 + return $resolved_as_bundle[ $handle ];
2422 2468 }
2423 - $payload = openstation_resolve_script_payload( $handle );
2469 + $payload = isset( $script_data[ $handle ] )
2470 + ? $script_data[ $handle ]
2471 + : openstation_resolve_script_payload( $handle );
2424 2472 if ( '' === $payload['url'] ) {
2473 + $resolved_as_bundle[ $handle ] = '';
2425 2474 return '';
2426 2475 }
2476 + $resolved_as_bundle[ $handle ] = $handle;
2477 + $deps = array();
2478 + foreach ( openstation_resolve_script_dependencies( $handle ) as $dep ) {
2479 + $dep_handle = (string) $dep['handle'];
2480 + unset( $dep['handle'] );
2481 + if ( ! isset( $script_data[ $dep_handle ] ) ) {
2482 + $dep['deps'] = array();
2483 + $script_data[ $dep_handle ] = $dep;
2484 + }
2485 + $deps[] = $dep_handle;
2486 + }
2487 + $payload['deps'] = $deps;
2427 2488 $script_data[ $handle ] = $payload;
2428 2489 return $handle;
2429 2490 };
2430 2491
@@ -2674,17 +2735,45 @@
2674 2735 return file_exists( ABSPATH . 'wp-admin/' . $file );
2675 2736 }
2676 2737
2677 2738 /**
2739 + * The admin URL a menu slug resolves against.
2740 + *
2741 + * Follows the admin the request is in: the network admin's own URL there,
2742 + * because its globals carry network slugs (`sites.php`, `settings.php`)
2743 + * that exist only under `wp-admin/network/`, and the site admin's
2744 + * everywhere else.
2745 + *
2746 + * The same answer `self_admin_url()` gives, without its filter. That
2747 + * filter receives the path, so a host can use it to send one screen
2748 + * somewhere else, and WordPress.com points `plugin-install.php` at its own
2749 + * installer. Resolved through it, the wp-admin original of a menu row the
2750 + * host replaced reads as off-site, and the dock drops it along with the
2751 + * replacement, which is how Plugins > Add Plugin disappears there.
2752 + *
2753 + * @param string $path Optional. Path relative to the admin URL.
2754 + * @return string Absolute admin URL.
2755 + */
2756 +function openstation_menu_admin_url( $path = '' ) {
2757 + if ( is_network_admin() ) {
2758 + return network_admin_url( $path );
2759 + }
2760 + if ( is_user_admin() ) {
2761 + return user_admin_url( $path );
2762 + }
2763 + return admin_url( $path );
2764 +}
2765 +
2766 +/**
2678 2767 * Converts a menu item slug to a full admin URL.
2679 2768 *
2680 - * Resolution goes through `self_admin_url()`, not `admin_url()`: in the
2681 - * network admin the same globals carry network slugs (`sites.php`,
2682 - * `settings.php`) that exist only under `wp-admin/network/`.
2769 + * Resolution goes through {@see openstation_menu_admin_url()}, which
2770 + * follows the admin the request is in without passing through the
2771 + * filterable `self_admin_url()`.
2683 2772 *
2684 2773 * Handles three slug shapes:
2685 2774 * 1. Direct file references (`edit.php`, `upload.php`) — passed
2686 - * through `self_admin_url()` as-is.
2775 + * through `openstation_menu_admin_url()` as-is.
2687 2776 * 2. Plain plugin page slugs (`my-plugin`) — routed through
2688 2777 * `admin.php?page=<slug>` with the slug `rawurlencode()`d.
2689 2778 * 3. Plugin page slugs that embed extra query parameters
2690 2779 * (`wc-admin&path=/customers`) — split on the first `&`, the
@@ -2745,9 +2834,9 @@
2745 2834 if (
2746 2835 false !== strpos( $slug, '.php' ) &&
2747 2836 ( ! isset( $_parent_pages[ $slug ] ) || openstation_is_admin_file_slug( $slug ) )
2748 2837 ) {
2749 - return esc_url_raw( self_admin_url( $slug ) );
2838 + return esc_url_raw( openstation_menu_admin_url( $slug ) );
2750 2839 }
2751 2840
2752 2841 // Plugin page slug with embedded query parameters
2753 2842 // (e.g., 'wc-admin&path=/customers'). Split the page slug from
@@ -2787,9 +2876,9 @@
2787 2876 $host = add_query_arg( 'page', $slug, $parent_slug );
2788 2877 }
2789 2878 }
2790 2879
2791 - $url = self_admin_url( $host );
2880 + $url = openstation_menu_admin_url( $host );
2792 2881 if ( ! empty( $extra_args ) ) {
2793 2882 $url = add_query_arg( $extra_args, $url );
2794 2883 }
2795 2884 return esc_url_raw( $url );