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 +178 -47 1.1.51.1.10 View file →
@@ -454,10 +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 - * Both `admin_url()` and `home_url()` hosts count as ours: a site can
459 - * run its admin on a different domain than its front end.
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.
460 462 *
461 463 * @param string $url Absolute URL, as returned by `openstation_menu_item_url()`.
462 464 * @return bool True when the URL is off-site.
463 465 */
@@ -466,9 +468,9 @@
466 468 $external = false;
467 469
468 470 if ( $host ) {
469 471 $ours = array();
470 - foreach ( array( admin_url(), home_url() ) as $known ) {
472 + foreach ( array( openstation_menu_admin_url(), admin_url(), home_url() ) as $known ) {
471 473 $known_host = wp_parse_url( $known, PHP_URL_HOST );
472 474 if ( $known_host ) {
473 475 $ours[] = strtolower( $known_host );
474 476 }
@@ -817,8 +819,19 @@
817 819 'link-manager.php', // Link manager (legacy)
818 820 'update-core.php', // Dashboard > Updates
819 821 );
820 822
823 + // The two top-level network menus the site admin has no filename
824 + // for: without them, Sites and Settings sat in the apps zone while
825 + // Dashboard, Users, Themes and Plugins — whose filenames the site
826 + // admin shares — grouped correctly. Gated on the context, since
827 + // `settings.php` is plausible enough as a plugin's own top-level
828 + // slug that claiming it everywhere would misfile it.
829 + if ( is_network_admin() ) {
830 + $core_files[] = 'sites.php';
831 + $core_files[] = 'settings.php';
832 + }
833 +
821 834 return in_array( $base, $core_files, true );
822 835 }
823 836
824 837 /**
@@ -1650,8 +1663,15 @@
1650 1663 'url' => network_admin_url( 'update-core.php' ),
1651 1664 );
1652 1665 }
1653 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 +
1654 1674 // A cheap structural fingerprint of the admin menu the shell uses to
1655 1675 // decide whether a live refresh is warranted. Shipped in every full
1656 1676 // payload so the shell can seed / update its last-known signature
1657 1677 // without recomputing it client-side (which would risk drift from
@@ -1853,8 +1873,12 @@
1853 1873 if ( $dep_handle === $handle ) {
1854 1874 continue;
1855 1875 }
1856 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.
1857 1881 if ( '' === $payload['url']
1858 1882 && empty( $payload['before'] )
1859 1883 && empty( $payload['after'] )
1860 1884 && empty( $payload['l10n'] ) ) {
@@ -1859,8 +1883,17 @@
1859 1883 && empty( $payload['after'] )
1860 1884 && empty( $payload['l10n'] ) ) {
1861 1885 continue;
1862 1886 }
1887 + // The handle rides along because the shell needs it to decide
1888 + // whether the page already has this package. A URL is not
1889 + // enough: with Core's script concatenation on — the wp-admin
1890 + // default — every package below `wp-includes/js/` is served
1891 + // from one `load-scripts.php` blob and has no `<script src>`
1892 + // of its own to match against. Re-running `wp-hooks` because
1893 + // we could not see it replaces `window.wp.hooks`, and every
1894 + // subscriber registered at boot goes deaf. See
1895 + // `src/script-presence.ts`.
1863 1896 $payload['handle'] = (string) $dep_handle;
1864 1897 $out[] = $payload;
1865 1898 }
1866 1899 return $out;
@@ -1891,10 +1924,14 @@
1891 1924 * around the lazy `<script src>` in the same order
1892 1925 * `WP_Scripts::do_item()` would have used.
1893 1926 *
1894 1927 * Returns an empty payload (`array( 'url' => '' )`) when the handle
1895 - * is unregistered or has no source — callers treat that as "no
1896 - * 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.
1897 1934 *
1898 1935 * Shared between `openstation_register_window()` and
1899 1936 * `openstation_register_widget()` (and every other registration that
1900 1937 * relies on lazy script loading in the shell) because all of them
@@ -1922,20 +1959,31 @@
1922 1959 return $empty;
1923 1960 }
1924 1961 $registered = $wp_scripts->registered[ $handle ];
1925 1962 $src = is_string( $registered->src ) ? $registered->src : '';
1926 - if ( '' === $src ) {
1927 - return $empty;
1928 - }
1929 1963
1930 - // Normalize relative paths + attach cache-bust ver.
1931 - $resolved = $src;
1932 - if ( 0 === strpos( $resolved, '/' ) && 0 !== strpos( $resolved, '//' ) ) {
1933 - $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 + }
1934 1985 }
1935 - if ( ! empty( $registered->ver ) ) {
1936 - $resolved = add_query_arg( 'ver', $registered->ver, $resolved );
1937 - }
1938 1986
1939 1987 // Harvest `extra` data the lazy-load path would otherwise drop.
1940 1988 $before = array();
1941 1989 $after = array();
@@ -1970,9 +2018,9 @@
1970 2018 // `wp.i18n.setLocaleData( JSON, 'domain' )` snippet that the print
1971 2019 // pipeline emits before the script body. `print_translations(
1972 2020 // $handle, false )` returns the snippet without echoing.
1973 2021 $translations = '';
1974 - if ( method_exists( $wp_scripts, 'print_translations' ) ) {
2022 + if ( '' !== $resolved && method_exists( $wp_scripts, 'print_translations' ) ) {
1975 2023 $captured = $wp_scripts->print_translations( $handle, false );
1976 2024 if ( is_string( $captured ) ) {
1977 2025 $translations = $captured;
1978 2026 }
@@ -2083,12 +2131,15 @@
2083 2131 * time the palette is invoked (`src/commands/palette-assets.ts`).
2084 2132 *
2085 2133 * Handles with no `src` (pure aggregators) are kept whenever they
2086 2134 * carry inline data; dropping them would lose middleware and locale
2087 - * setup the chain depends on. Handles another plugin already
2088 - * enqueued at boot print normally and are skipped client-side by a
2089 - * same-path DOM sniff — the manifest deliberately lists them anyway,
2090 - * because which ones those are differs per site and per screen.
2135 + * setup the chain depends on. Handles the boot page already printed
2136 + * are skipped client-side, by handle as well as by path so that a
2137 + * package Core concatenated into `load-scripts.php` is recognized
2138 + * (`src/script-presence.ts`) — the manifest deliberately lists them
2139 + * anyway, because which ones those are differs per site and per
2140 + * screen. Each entry therefore carries its `handle`, and that is
2141 + * load-bearing rather than informational.
2091 2142 *
2092 2143 * Returns `null` on pre-6.9 sites (no Core palette to defer).
2093 2144 *
2094 2145 * @return array{scripts:array<int,array<string,mixed>>,styles:array<int,array<string,mixed>>}|null
@@ -2143,24 +2194,16 @@
2143 2194 $script_probe->done = array();
2144 2195 $script_probe->all_deps( $script_roots );
2145 2196 foreach ( $script_probe->to_do as $handle ) {
2146 2197 $payload = openstation_resolve_script_payload( $handle );
2147 - if ( '' === $payload['url'] ) {
2148 - // Src-less aggregator — keep it only for its inline data.
2149 - $registered = isset( $scripts->registered[ $handle ] ) ? $scripts->registered[ $handle ] : null;
2150 - if ( $registered ) {
2151 - foreach ( array( 'before', 'after' ) as $position ) {
2152 - if ( isset( $registered->extra[ $position ] ) && is_array( $registered->extra[ $position ] ) ) {
2153 - $payload[ $position ] = array_values( array_filter( array_map( 'strval', $registered->extra[ $position ] ) ) );
2154 - }
2155 - }
2156 - if ( ! empty( $registered->extra['data'] ) && is_string( $registered->extra['data'] ) ) {
2157 - $payload['l10n'][] = $registered->extra['data'];
2158 - }
2159 - }
2160 - if ( empty( $payload['before'] ) && empty( $payload['after'] ) && empty( $payload['l10n'] ) ) {
2161 - continue;
2162 - }
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;
2163 2206 }
2164 2207 // Core's `initializeCommandPalette( {…} )` inline embeds the
2165 2208 // serialized admin-menu command list — ~20 KB that the boot
2166 2209 // page ALREADY carries as `window.__openStationMenuCommands`
@@ -2278,9 +2321,9 @@
2278 2321 _doing_it_wrong(
2279 2322 esc_html( $function_name ),
2280 2323 sprintf(
2281 2324 /* translators: 1: kind ("Command"/"Settings-tab"/"Title-bar button"), 2: handle. */
2282 - esc_html__( '%1$s script handle "%2$s" is not registered with WordPress (no `wp_register_script` call found). The script will not load.', 'desktop-mode' ),
2325 + esc_html__( '%1$s script handle "%2$s" could not be resolved: no `wp_register_script( \'%2$s\', … )` call had run by the time the shell harvested its payload. Register the handle on `admin_enqueue_scripts` at priority 5 or earlier — the harvest itself runs at priority 10, and a handle registered alongside it may or may not exist yet depending on plugin load order. Until then the script will not load.', 'desktop-mode' ),
2283 2326 esc_html( $kind ),
2284 2327 esc_html( $handle )
2285 2328 ),
2286 2329 '0.8.1'
@@ -2334,12 +2377,16 @@
2334 2377 * harvested `wp_localize_script` / `wp_add_inline_script` /
2335 2378 * translations, see `openstation_resolve_script_payload()` — lives
2336 2379 * ONCE per handle in `scriptData`, and the shell joins the two on
2337 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.
2338 2385 *
2339 2386 * The split exists because script data is a property of the HANDLE,
2340 - * not of the window: Posts, Pages, Users and Profile all ride
2341 - * `os-posts-window`, and inlining each entry's resolved copy
2387 + * not of the window: every App Framework window rides
2388 + * `openstation-app-runtime`, and inlining each entry's resolved copy
2342 2389 * serialized the same localize blobs and the same shared config set
2343 2390 * four times over — `scriptL10n` alone was ~100 KB of the boot
2344 2391 * payload, most of it repetition. The synthesized
2345 2392 * `openStationWindowConfig[ id ]` assignments group by handle for
@@ -2352,9 +2399,9 @@
2352 2399 * Style data stays inline on the entries — it never had a
2353 2400 * duplication problem worth a second map ( companion styles across
2354 2401 * the whole registry total ~2 KB ).
2355 2402 *
2356 - * @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[]}>}
2357 2404 */
2358 2405 function openstation_collect_native_windows_payload() {
2359 2406 $empty = array(
2360 2407 'windows' => array(),
@@ -2362,31 +2409,83 @@
2362 2409 );
2363 2410 if ( ! function_exists( 'openstation_native_window_registry' ) ) {
2364 2411 return $empty;
2365 2412 }
2413 +
2366 2414 $registry = openstation_native_window_registry();
2367 2415 if ( ! is_array( $registry ) ) {
2368 2416 return $empty;
2369 2417 }
2370 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 +
2371 2435 $script_data = array();
2372 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 +
2373 2445 // Resolve a handle into the map, once. Returns the handle when it
2374 2446 // resolved to something loadable, '' when it did not (never
2375 2447 // registered, no src) — the same silent drop the inline shape
2376 2448 // applied to companions and tab scripts.
2377 - $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 ) {
2378 2462 $handle = (string) $handle;
2379 2463 if ( '' === $handle ) {
2380 2464 return '';
2381 2465 }
2382 - if ( isset( $script_data[ $handle ] ) ) {
2383 - return $handle;
2466 + if ( isset( $resolved_as_bundle[ $handle ] ) ) {
2467 + return $resolved_as_bundle[ $handle ];
2384 2468 }
2385 - $payload = openstation_resolve_script_payload( $handle );
2469 + $payload = isset( $script_data[ $handle ] )
2470 + ? $script_data[ $handle ]
2471 + : openstation_resolve_script_payload( $handle );
2386 2472 if ( '' === $payload['url'] ) {
2473 + $resolved_as_bundle[ $handle ] = '';
2387 2474 return '';
2388 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;
2389 2488 $script_data[ $handle ] = $payload;
2390 2489 return $handle;
2391 2490 };
2392 2491
@@ -2636,13 +2735,45 @@
2636 2735 return file_exists( ABSPATH . 'wp-admin/' . $file );
2637 2736 }
2638 2737
2639 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 +/**
2640 2767 * Converts a menu item slug to a full admin URL.
2641 2768 *
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()`.
2772 + *
2642 2773 * Handles three slug shapes:
2643 2774 * 1. Direct file references (`edit.php`, `upload.php`) — passed
2644 - * through `admin_url()` as-is.
2775 + * through `openstation_menu_admin_url()` as-is.
2645 2776 * 2. Plain plugin page slugs (`my-plugin`) — routed through
2646 2777 * `admin.php?page=<slug>` with the slug `rawurlencode()`d.
2647 2778 * 3. Plugin page slugs that embed extra query parameters
2648 2779 * (`wc-admin&path=/customers`) — split on the first `&`, the
@@ -2703,9 +2834,9 @@
2703 2834 if (
2704 2835 false !== strpos( $slug, '.php' ) &&
2705 2836 ( ! isset( $_parent_pages[ $slug ] ) || openstation_is_admin_file_slug( $slug ) )
2706 2837 ) {
2707 - return esc_url_raw( admin_url( $slug ) );
2838 + return esc_url_raw( openstation_menu_admin_url( $slug ) );
2708 2839 }
2709 2840
2710 2841 // Plugin page slug with embedded query parameters
2711 2842 // (e.g., 'wc-admin&path=/customers'). Split the page slug from
@@ -2745,9 +2876,9 @@
2745 2876 $host = add_query_arg( 'page', $slug, $parent_slug );
2746 2877 }
2747 2878 }
2748 2879
2749 - $url = admin_url( $host );
2880 + $url = openstation_menu_admin_url( $host );
2750 2881 if ( ! empty( $extra_args ) ) {
2751 2882 $url = add_query_arg( $extra_args, $url );
2752 2883 }
2753 2884 return esc_url_raw( $url );