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/routing.php +126 -2 1.1.41.1.10 View file →
@@ -15,12 +15,14 @@
15 15 * always present by the time WordPress wants them.
16 16 *
17 17 * Functions in this file:
18 18 * - {@see openstation_url_is_same_admin()} — same-origin admin URL predicate
19 + * - {@see openstation_url_is_page_less_admin_php()} — "renders nothing" predicate
19 20 * - {@see openstation_resolve_admin_target()} — admin filename → URL resolver
20 21 * - {@see openstation_admin_target_allowlist()} — wp-admin filename allowlist
21 22 * - {@see openstation_is_chromeless_request()} — chromeless request detection
22 23 * - {@see openstation_is_classic_request()} — classic-override request detection
24 + * - {@see openstation_is_subresource_request()} — sub-resource fetch detection
23 25 * - {@see openstation_chromeless_hide_admin_bar()} — `show_admin_bar` filter
24 26 * - {@see openstation_chromeless_suppress_admin_bar()} — `admin_init` action
25 27 * - {@see openstation_chromeless_preserve_redirect()} — `wp_redirect` filter
26 28 * - {@see openstation_classic_preserve_redirect()} — `wp_redirect` filter
@@ -74,8 +76,52 @@
74 76 return 0 === strpos( $url_path, $admin_path );
75 77 }
76 78
77 79 /**
80 + * Whether `$url` addresses `wp-admin/admin.php` with no `page` arg.
81 + *
82 + * `admin.php` is core's plugin-screen bootstrap, and the allowlist in
83 + * {@see openstation_admin_target_allowlist()} accepts it for exactly
84 + * that reason — every plugin screen in the admin lives there. Without
85 + * a `page` arg, though, there is no screen to dispatch to: core falls
86 + * through the last `else` in `wp-admin/admin.php`, fires a couple of
87 + * back-compat `load-*` hooks, and returns 200 with an empty body,
88 + * having required neither `admin-header.php` nor `admin-footer.php`.
89 + *
90 + * So the URL resolves, passes every same-origin and allowlist check,
91 + * and renders nothing. Callers that are about to turn a URL into a
92 + * window or a redirect target use this to refuse it and fall back.
93 + *
94 + * Accepts absolute URLs and request-URI-shaped paths, mirroring
95 + * {@see openstation_url_is_shell_screen()}, whose guard this sits
96 + * beside at every call site.
97 + *
98 + * @param string $url URL or path to test.
99 + * @return bool
100 + */
101 +function openstation_url_is_page_less_admin_php( $url ) {
102 + if ( ! is_string( $url ) || '' === $url ) {
103 + return false;
104 + }
105 +
106 + $path = wp_parse_url( $url, PHP_URL_PATH );
107 + if ( ! is_string( $path ) || 'admin.php' !== basename( $path ) ) {
108 + return false;
109 + }
110 +
111 + $query = wp_parse_url( $url, PHP_URL_QUERY );
112 + if ( ! is_string( $query ) || '' === $query ) {
113 + return true;
114 + }
115 +
116 + // `page=` present but empty is the same nothing: core only sets
117 + // `$plugin_page` from a non-empty `?page=`. An array (`page[]=x`)
118 + // is not a slug either.
119 + parse_str( $query, $args );
120 + return ! isset( $args['page'] ) || ! is_string( $args['page'] ) || '' === $args['page'];
121 +}
122 +
123 +/**
78 124 * Resolves an admin-page filename (e.g. `edit.php`) to its
79 125 * absolute admin URL, allowlisted against the canonical set of
80 126 * wp-admin top-level filenames.
81 127 *
@@ -84,12 +130,13 @@
84 130 * exist in the static allowlist. A regex-only check would accept
85 131 * `custom_admin_page.php` if a plugin named something that way;
86 132 * the explicit allowlist closes that.
87 133 *
88 - * @param string $file Bare admin filename (no path, no query string).
134 + * @param string $file Bare admin filename (no path, no query string).
135 + * @param bool $network Resolve against the network admin's own screens.
89 136 * @return string|WP_Error Absolute admin URL on success, `WP_Error` otherwise.
90 137 */
91 -function openstation_resolve_admin_target( $file ) {
138 +function openstation_resolve_admin_target( $file, $network = false ) {
92 139 $file = is_string( $file ) ? trim( $file ) : '';
93 140 if ( '' === $file ) {
94 141 return new WP_Error(
95 142 'openstation_empty_target',
@@ -114,8 +161,17 @@
114 161 __( 'Admin target must be a plain .php filename.', 'desktop-mode' )
115 162 );
116 163 }
117 164
165 + if ( $network ) {
166 + return in_array( strtolower( $file ), openstation_network_admin_target_allowlist(), true )
167 + ? network_admin_url( $file )
168 + : new WP_Error(
169 + 'openstation_unknown_target',
170 + __( 'Admin target does not exist.', 'desktop-mode' )
171 + );
172 + }
173 +
118 174 if ( ! in_array( strtolower( $file ), openstation_admin_target_allowlist(), true ) ) {
119 175 return new WP_Error(
120 176 'openstation_unknown_target',
121 177 __( 'Admin target does not exist.', 'desktop-mode' )
@@ -125,8 +181,45 @@
125 181 return admin_url( $file );
126 182 }
127 183
128 184 /**
185 + * Canonical `wp-admin/network/` filenames a target may resolve to.
186 + *
187 + * The network admin's own screens, and only those: the site allowlist
188 + * cannot stand in for it, since the two directories share filenames
189 + * that mean different things (`users.php` is everyone on the network
190 + * here, one site's users there).
191 + *
192 + * @return string[]
193 + */
194 +function openstation_network_admin_target_allowlist() {
195 + return array(
196 + 'index.php',
197 + 'sites.php',
198 + 'site-new.php',
199 + 'site-info.php',
200 + 'site-users.php',
201 + 'site-themes.php',
202 + 'site-settings.php',
203 + 'users.php',
204 + 'user-new.php',
205 + 'themes.php',
206 + 'theme-install.php',
207 + 'plugins.php',
208 + 'plugin-install.php',
209 + 'plugin-editor.php',
210 + 'settings.php',
211 + 'setup.php',
212 + 'upgrade.php',
213 + 'update-core.php',
214 + 'about.php',
215 + 'credits.php',
216 + 'freedoms.php',
217 + 'privacy.php',
218 + );
219 +}
220 +
221 +/**
129 222 * Returns the allowlist of canonical wp-admin top-level
130 223 * filenames that {@see openstation_resolve_admin_target()}
131 224 * accepts.
132 225 *
@@ -328,8 +421,39 @@
328 421 return false;
329 422 }
330 423 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request flag.
331 424 return '1' === sanitize_text_field( wp_unslash( $_GET[ OPENSTATION_CLASSIC_FLAG ] ) );
425 +}
426 +
427 +/**
428 + * Checks whether the browser is fetching this request as a
429 + * sub-resource of some page rather than navigating to it.
430 + *
431 + * Admin URLs serve more than pages. Jetpack's admin-bar sparkline is
432 + * an `<img>` whose src is `admin.php?page=stats&noheader&proxy&chart=…`:
433 + * core's `admin.php` skips the header on `noheader` and the page hook
434 + * echoes PNG bytes. The Jetpack Stats screen loads its report body the
435 + * same way, over XHR. Treating such a request as "a user landing on a
436 + * plain admin page" and forwarding it into the desktop hands the
437 + * consumer an HTML document instead: the admin bar then draws a broken
438 + * image with the alt text where the chart should be.
439 + *
440 + * `Sec-Fetch-Mode` is the browser's own answer, set by the user agent
441 + * and immune to script. `navigate` is a document or frame load, the
442 + * only kind of request worth forwarding into the desktop; `cors`,
443 + * `no-cors`, `same-origin` and `websocket` are sub-resource fetches.
444 + * A missing header (an old browser, a proxy that strips it) answers
445 + * false: not known to be a sub-resource, so callers keep behaving as
446 + * they always did.
447 + *
448 + * @return bool True when the request is a sub-resource fetch.
449 + */
450 +function openstation_is_subresource_request() {
451 + if ( empty( $_SERVER['HTTP_SEC_FETCH_MODE'] ) ) {
452 + return false;
453 + }
454 + $mode = strtolower( sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_FETCH_MODE'] ) ) );
455 + return '' !== $mode && 'navigate' !== $mode;
332 456 }
333 457
334 458 /**
335 459 * Disables the admin bar on chromeless (iframe) requests.