PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.4
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.4
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
desktop-mode / includes / portal.php

portal.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.4, at includes/portal.php

605 lines 22.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — `/openstation` Portal Entry Point.
4 *
5 * Registers `/openstation` as a shareable URL that behaves like the
6 * front door of the desktop UI:
7 * 1. Logged-out users are bounced through `wp-login.php` with a
8 * redirect back to `/openstation/`.
9 * 2. Logged-in users with basic admin-read capability have the
10 * `desktop_mode_mode` user-meta toggle auto-enabled on first visit,
11 * then are forwarded into `wp-admin` at whichever window was
12 * last focused in their saved session (or the dashboard as
13 * fallback).
14 *
15 * The URL is served virtually (no rewrite rules, no `.htaccess`
16 * surgery) by intercepting `parse_request` before WordPress routes the
17 * URL to 404. This keeps the plugin drop-in.
18 *
19 * @package OpenStation
20 */
21
22 defined( 'ABSPATH' ) || exit;
23
24 /** The URL path that triggers the portal handler. */
25 const OPENSTATION_PORTAL_PATH = 'openstation';
26
27 /**
28 * The pre-rebrand portal path, still accepted.
29 *
30 * The portal was reachable at `/desktop-mode/` before the rename, and
31 * that address is the kind of thing people bookmark or pin. It is not
32 * canonical: {@see openstation_portal_url()} always emits the current
33 * path, and a visit here forwards into wp-admin exactly as the canonical
34 * path does, so the address bar self-corrects on the next hop.
35 *
36 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
37 * persisted or externally-visible identifier, so renaming it would
38 * orphan data already written by live installs (or break a live
39 * URL). The mismatch between this constant's name and its value is
40 * deliberate — it is NOT a half-finished rename.
41 */
42 const OPENSTATION_PORTAL_PATH_LEGACY = 'desktop-mode';
43
44 /**
45 * Query var the admin shell reads to know it was entered via the portal.
46 *
47 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
48 * persisted or externally-visible identifier, so renaming it would
49 * orphan data already written by live installs (or break a live
50 * URL). The mismatch between this constant's name and its value is
51 * deliberate — it is NOT a half-finished rename.
52 */
53 const OPENSTATION_PORTAL_FLAG = 'desktop_mode_portal';
54
55 /**
56 * Query var set on portal redirects whose landing page came from an
57 * explicit `?target=…` URL the user (or a redirect chain originating
58 * from a click) provided — as opposed to the portal picking the
59 * session's focused window or the default-window fallback.
60 *
61 * The shell uses this to distinguish "user expressed navigation intent
62 * toward this URL" (open it) from "portal had to forward somewhere"
63 * (don't disturb the restored session).
64 *
65 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
66 * persisted or externally-visible identifier, so renaming it would
67 * orphan data already written by live installs (or break a live
68 * URL). The mismatch between this constant's name and its value is
69 * deliberate — it is NOT a half-finished rename.
70 */
71 const OPENSTATION_PORTAL_INTENT_FLAG = 'desktop_mode_portal_intent';
72
73 /**
74 * Query var set by the window-title-bar "Detach" action. Tells the
75 * admin_init redirect to skip portal forwarding for this request so the
76 * user can view the page as classic wp-admin in a new tab even when
77 * OpenStation is globally enabled for their account.
78 *
79 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
80 * persisted or externally-visible identifier, so renaming it would
81 * orphan data already written by live installs (or break a live
82 * URL). The mismatch between this constant's name and its value is
83 * deliberate — it is NOT a half-finished rename.
84 */
85 const OPENSTATION_CLASSIC_FLAG = 'desktop_mode_classic';
86
87 /**
88 * Returns the canonical portal URL, e.g. `https://example.com/openstation/`.
89 *
90 * @return string
91 */
92 function openstation_portal_url() {
93 return home_url( '/' . OPENSTATION_PORTAL_PATH . '/' );
94 }
95
96 /**
97 * Intercepts requests to `/openstation` and forwards them into the admin.
98 *
99 * Hooks on `parse_request` — early enough to pre-empt 404 handling but
100 * late enough that `is_user_logged_in()` is reliable.
101 *
102 * @param WP $wp Current WordPress environment instance.
103 */
104 function openstation_handle_portal_request( $wp ) {
105 unset( $wp );
106
107 if ( ! openstation_is_portal_request() ) {
108 return;
109 }
110
111 // Logged-out: bounce through login, returning to the portal URL.
112 if ( ! is_user_logged_in() ) {
113 wp_safe_redirect( wp_login_url( openstation_portal_url() ) );
114 exit;
115 }
116
117 // Require basic admin-read capability so subscribers of sites that
118 // blocked `read` from admin don't land in a broken window.
119 if ( ! current_user_can( 'read' ) ) {
120 wp_die(
121 esc_html__( 'Sorry, you are not allowed to access the WordPress desktop.', 'desktop-mode' ),
122 '',
123 array( 'response' => 403 )
124 );
125 }
126
127 $user_id = get_current_user_id();
128
129 /**
130 * Filters whether visiting the `/openstation` portal should auto-enable
131 * OpenStation for the current user.
132 *
133 * Default: true — the portal is an explicit opt-in action, so flipping
134 * the user meta mirrors the intent of visiting the URL.
135 *
136 * @param bool $auto_enable Whether to auto-enable OpenStation.
137 * @param int $user_id The current user's ID.
138 */
139 $auto_enable = apply_filters( 'openstation_portal_auto_enable', true, $user_id );
140
141 // CSRF guard: only flip user-meta when the request is a same-origin
142 // top-level navigation. The portal is a GET URL by design (users
143 // follow shared `/openstation/` links), so we can't require a nonce
144 // — but we can require that the navigation originated from the
145 // same site (or a typed/bookmarked URL with no Referer/Sec-Fetch-
146 // Site). Off-origin hits still redirect into admin so shared
147 // links keep working; they just don't silently mutate user-meta.
148 if ( $auto_enable && openstation_portal_is_same_origin_navigation() && '1' !== get_user_meta( $user_id, 'desktop_mode_mode', true ) ) {
149 update_user_meta( $user_id, 'desktop_mode_mode', '1' );
150 }
151
152 // Pick the landing page. Priority:
153 // 1. Explicit `target` query arg, if same-origin wp-admin URL.
154 // This is how `openstation_redirect_plain_admin_to_portal` preserves
155 // the user's navigation intent when they follow a link to a
156 // specific admin page (e.g. profile.php).
157 // 2. Last-focused window from the saved session.
158 // 3. Dashboard fallback.
159 $target = '';
160 $has_intent = false;
161 if ( ! empty( $_GET['target'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
162 // `esc_url_raw`, NOT `sanitize_text_field`: the latter strips
163 // every `%XX` percent-encoded sequence from its input as an XSS
164 // safeguard, which mangles request URIs that legitimately carry
165 // encoded slashes (e.g. `plugin=dir%2Ffile.php`). The downstream
166 // `openstation_sanitize_portal_target` validates the URL
167 // rigorously (scheme rejection, traversal rejection, and a
168 // hardcoded allowlist of canonical wp-admin filenames — see
169 // `openstation_admin_target_allowlist()`) so we don't lose
170 // any real safety by skipping `sanitize_text_field` here.
171 $target = openstation_sanitize_portal_target( esc_url_raw( wp_unslash( $_GET['target'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
172 if ( '' !== $target ) {
173 $has_intent = true;
174 }
175 }
176 if ( '' === $target ) {
177 $target = openstation_portal_entry_url( $user_id );
178 }
179
180 // Flag the forward so the shell can stamp the address bar back to
181 // /openstation/ via history.replaceState once it has loaded.
182 $target = add_query_arg( OPENSTATION_PORTAL_FLAG, '1', $target );
183
184 // Second flag: the redirect resolved from an explicit `target`, so
185 // the shell should treat the resulting `currentPage` as user
186 // intent and auto-open it on top of the restored session. Without
187 // this, a bare `/openstation/` visit and a portal-redirected
188 // admin-bar click would be indistinguishable downstream.
189 if ( $has_intent ) {
190 $target = add_query_arg( OPENSTATION_PORTAL_INTENT_FLAG, '1', $target );
191 }
192
193 wp_safe_redirect( $target );
194 exit;
195 }
196 add_action( 'parse_request', 'openstation_handle_portal_request' );
197
198 /**
199 * Decides whether the current request to the portal can mutate
200 * user-meta safely (same-origin) or should only redirect (cross-
201 * origin, possibly CSRF).
202 *
203 * Logic mirrors the `Sec-Fetch-Site` heuristic browsers use:
204 *
205 * - `Sec-Fetch-Site: same-origin | same-site | none` → trusted
206 * (the request originated from this site, or from a typed URL
207 * / bookmark with no referrer info).
208 * - `Sec-Fetch-Site: cross-site` → untrusted (a third-party page
209 * pointed the user at the portal — could be an `<img>` tag).
210 * - Header missing (older browsers): fall back to `Referer` —
211 * same host or empty referrer is trusted, anything else isn't.
212 *
213 * @return bool
214 */
215 function openstation_portal_is_same_origin_navigation() {
216 if ( ! empty( $_SERVER['HTTP_SEC_FETCH_SITE'] ) ) {
217 $site = strtolower( sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_FETCH_SITE'] ) ) );
218 return in_array( $site, array( 'same-origin', 'same-site', 'none' ), true );
219 }
220
221 if ( empty( $_SERVER['HTTP_REFERER'] ) ) {
222 return true;
223 }
224
225 $referer_host = wp_parse_url( esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ), PHP_URL_HOST );
226 $home_host = wp_parse_url( home_url(), PHP_URL_HOST );
227
228 if ( ! is_string( $referer_host ) || '' === $referer_host ) {
229 return true;
230 }
231
232 return is_string( $home_host ) && strtolower( $referer_host ) === strtolower( $home_host );
233 }
234
235 /**
236 * Detects whether the current request is for the portal URL.
237 *
238 * Strips any query string and trailing slash and compares against
239 * `/openstation` relative to the site's home path. The pre-rebrand
240 * `/desktop-mode` path is accepted too, so bookmarks made before the
241 * rename still land in the shell.
242 *
243 * @return bool
244 */
245 function openstation_is_portal_request() {
246 if ( empty( $_SERVER['REQUEST_URI'] ) ) {
247 return false;
248 }
249
250 // `esc_url_raw` instead of `sanitize_text_field` so percent-encoded
251 // chars in the URI (notably `%2F` from query-arg slashes) survive
252 // long enough for `wp_parse_url` to split path / query correctly.
253 $uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
254 $path = wp_parse_url( $uri, PHP_URL_PATH );
255 if ( ! is_string( $path ) ) {
256 return false;
257 }
258
259 $home_path = wp_parse_url( home_url( '/' ), PHP_URL_PATH );
260 $home_path = is_string( $home_path ) ? rtrim( $home_path, '/' ) : '';
261
262 $path = '/' . ltrim( rtrim( $path, '/' ), '/' );
263
264 return in_array(
265 $path,
266 array(
267 $home_path . '/' . OPENSTATION_PORTAL_PATH,
268 $home_path . '/' . OPENSTATION_PORTAL_PATH_LEGACY,
269 ),
270 true
271 );
272 }
273
274 /**
275 * Forwards plain `/wp-admin/...` requests to the `/openstation/` portal
276 * when the portal would land the user somewhere other than here.
277 *
278 * Why: the portal honors the saved session's focused window, so a user
279 * who follows a link the portal can't resolve — a network-admin URL, a
280 * path outside the wp-admin allowlist — is better off on their restored
281 * desktop than on a page the shell can't place.
282 *
283 * Why NOT unconditionally: for the ordinary case the forward is a round
284 * trip to nowhere. The portal resolves `?target=` straight back to the
285 * URL we are already serving and redirects here with
286 * `desktop_mode_portal=1&desktop_mode_portal_intent=1` — a flag pair
287 * every consumer reads as `fromPortal && ! fromPortalIntent`, i.e. as
288 * indistinguishable from no flags at all. Two full WordPress bootstraps
289 * bought nothing. The shell does not need the portal to reach it: it
290 * enqueues on any admin page where {@see openstation_is_enabled()}, and
291 * the address bar is deliberately no longer normalized to
292 * `/openstation/` (see the `history.replaceState` note in
293 * `src/desktop.ts` — the round trip is exactly what made reloads flash).
294 * So {@see openstation_portal_forward_is_redundant()} answers the
295 * portal's question locally and we render in place when the answer is
296 * "right here."
297 *
298 * Narrowly scoped to bail on every automated or sub-request entry point
299 * — AJAX, REST, cron, admin-post.php, non-GET methods — so the hook
300 * can't corrupt a form submission or break an API call.
301 *
302 * Disable via the `openstation_admin_redirect_to_portal` filter (return
303 * false). Passthrough kicks in automatically when the current request
304 * is chromeless or already carries the portal flag.
305 */
306 function openstation_redirect_plain_admin_to_portal() {
307 if ( ! openstation_is_enabled() ) {
308 return;
309 }
310 if ( openstation_is_chromeless_request() ) {
311 return;
312 }
313 if ( wp_doing_ajax() || wp_doing_cron() ) {
314 return;
315 }
316 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
317 return;
318 }
319 if ( ! empty( $_SERVER['REQUEST_METHOD'] ) && 'GET' !== strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) ) {
320 return;
321 }
322
323 // The portal handler adds this flag after it forwards into admin.
324 // Bailing here keeps us out of an infinite redirect loop.
325 if ( ! empty( $_GET[ OPENSTATION_PORTAL_FLAG ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
326 return;
327 }
328
329 // The "Detach to new tab" button tags its URL with this flag so the
330 // user can view one admin page classically without disabling desktop
331 // mode account-wide. Only affects the single request — subsequent
332 // navigations inside the tab lose the flag and follow normal rules.
333 if ( ! empty( $_GET[ OPENSTATION_CLASSIC_FLAG ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
334 return;
335 }
336
337 // admin-post.php and admin-ajax.php handle form submissions and JSON
338 // endpoints; redirecting them would break the call.
339 global $pagenow;
340 if ( in_array( $pagenow, array( 'admin-post.php', 'admin-ajax.php' ), true ) ) {
341 return;
342 }
343
344 /**
345 * Filters whether plain admin URLs should redirect to the portal
346 * when OpenStation is active.
347 *
348 * @param bool $redirect Whether to redirect. Default true.
349 * @param int $user_id The current user's ID.
350 */
351 $redirect = apply_filters( 'openstation_admin_redirect_to_portal', true, get_current_user_id() );
352 if ( ! $redirect ) {
353 return;
354 }
355
356 // `esc_url_raw` instead of `sanitize_text_field`: the latter strips
357 // every `%XX` percent-encoded sequence, which corrupts URIs whose
358 // query string legitimately carries an encoded slash — e.g. WP's
359 // own `plugins.php?action=activate&plugin=dir%2Ffile.php` activate
360 // link. The portal handler will validate this target downstream.
361 $target = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
362 $target = is_string( $target ) ? $target : '';
363
364 if ( openstation_portal_forward_is_redundant( $target ) ) {
365 /**
366 * Filters whether to skip a portal forward that would resolve
367 * back to the URL already being served.
368 *
369 * Default: true — the forward costs two extra WordPress
370 * bootstraps and lands on the same page with flags the shell
371 * reads as a no-op. Return false to force the round trip, e.g.
372 * for a plugin that hooks `openstation_handle_portal_request`
373 * for its own side effects and needs it to run on every admin
374 * entry.
375 *
376 * @param bool $skip Whether to skip the redundant forward.
377 * @param string $request_uri The current request URI.
378 */
379 if ( apply_filters( 'openstation_skip_redundant_portal_forward', true, $target ) ) {
380 return;
381 }
382 }
383
384 // Preserve the original target on the portal redirect. Without this,
385 // navigating to a specific admin page (profile.php, plugins.php, any
386 // deep link) loses the user's intent — the portal would forward them
387 // to whichever window was last focused instead of the page they asked
388 // for. The portal handler reads `target`, validates it's same-origin
389 // wp-admin, and uses it as the entry URL.
390 $portal_url = openstation_portal_url();
391 if ( '' !== $target ) {
392 $portal_url = add_query_arg( 'target', rawurlencode( $target ), $portal_url );
393 }
394
395 wp_safe_redirect( $portal_url );
396 exit;
397 }
398 add_action( 'admin_init', 'openstation_redirect_plain_admin_to_portal' );
399
400 /**
401 * Whether forwarding this request through `/openstation/` would land
402 * the user straight back on the URL already being served.
403 *
404 * Answers locally, and without the HTTP round trip, the same question
405 * {@see openstation_handle_portal_request()} answers after two full
406 * WordPress bootstraps. True means the forward is pure overhead and the
407 * caller should render in place instead.
408 *
409 * Deliberately conservative: every "don't know" answers false, so the
410 * forward survives wherever the portal might genuinely choose a
411 * different destination.
412 *
413 * 1. The path must resolve through the same wp-admin allowlist the
414 * portal validates `?target=` against. Anything that list rejects
415 * — a `network/` or `user/` sub-path on multisite, a filename that
416 * isn't canonical wp-admin — makes the portal fall back to the
417 * session's focused window, which is a real change of destination.
418 * 2. The resolved filename must be the file this request is actually
419 * serving. If `$pagenow` disagrees with the URL path then a
420 * rewrite is in play and we can't claim to know what renders here.
421 * 3. The query must survive intact. The portal drops
422 * `openstation_chromeless`, both portal flags and `target` from
423 * the URL it rebuilds, so a request carrying any of them comes
424 * back as a different URL.
425 *
426 * @param string $request_uri The current request URI, unslashed.
427 * @return bool True when the portal would resolve this URL to itself.
428 */
429 function openstation_portal_forward_is_redundant( $request_uri ) {
430 global $pagenow;
431
432 if ( ! is_string( $request_uri ) || '' === $request_uri ) {
433 return false;
434 }
435
436 $path = wp_parse_url( $request_uri, PHP_URL_PATH );
437 if ( ! is_string( $path ) || '' === $path ) {
438 return false;
439 }
440
441 $admin_path = wp_parse_url( admin_url(), PHP_URL_PATH );
442 $admin_path = is_string( $admin_path ) ? $admin_path : '/wp-admin/';
443 if ( 0 !== strpos( $path, $admin_path ) ) {
444 return false;
445 }
446
447 $file = ltrim( (string) substr( $path, strlen( $admin_path ) ), '/' );
448 if ( '' === $file ) {
449 $file = 'index.php';
450 }
451
452 // 1. The portal's allowlist has to accept it.
453 if ( is_wp_error( openstation_resolve_admin_target( $file ) ) ) {
454 return false;
455 }
456
457 // 2. …and it has to be the page we are actually serving.
458 if ( ! is_string( $pagenow ) || strtolower( $file ) !== strtolower( $pagenow ) ) {
459 return false;
460 }
461
462 // 3. …carrying a query the portal would hand back unchanged.
463 $rewritten = array(
464 'openstation_chromeless',
465 OPENSTATION_PORTAL_FLAG,
466 OPENSTATION_PORTAL_INTENT_FLAG,
467 'target',
468 );
469 foreach ( $rewritten as $key ) {
470 if ( isset( $_GET[ $key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
471 return false;
472 }
473 }
474
475 return true;
476 }
477
478 /**
479 * Resolves the admin URL the portal should forward to for a given user.
480 *
481 * Looks up the user's session and returns the URL of the window flagged
482 * as `focused`. If the session is empty, has no focused window, or the
483 * focused window's URL isn't same-origin admin, falls back to the
484 * dashboard.
485 *
486 * The portal navigates the TOP window, not an iframe, so any chromeless
487 * `openstation_chromeless=1` flag baked into the stored URL is stripped — a leftover
488 * flag would land the user in a standalone chromeless page (no admin
489 * bar, no toggle, no way out) instead of the shell.
490 *
491 * @param int $user_id The user whose session to consult.
492 * @return string The admin URL to redirect to.
493 */
494 function openstation_portal_entry_url( $user_id ) {
495 $session = openstation_get_session( $user_id );
496
497 // User's configured default-window preference. When disabled, we
498 // still have to forward SOMEWHERE (the portal is an HTTP redirect),
499 // so we land on the Dashboard URL — but the shell detects the
500 // `enabled=false` state via the config and skips the auto-open,
501 // leaving the user with an empty desktop as they chose.
502 $default_window = openstation_get_default_window( $user_id );
503 $fallback = $default_window['url'];
504
505 // Native marker (e.g. "native:os-settings") is not a
506 // redirectable URL. The portal MUST forward somewhere — the
507 // redirect happens at HTTP level — so we land on the admin home
508 // and let the shell pick up `defaultWindow.url` from the config
509 // after init and call nativeWindows.openById( <slug> ).
510 if ( is_string( $fallback ) && 0 === strpos( $fallback, 'native:' ) ) {
511 $fallback = admin_url();
512 }
513
514 if ( empty( $session['focused'] ) || empty( $session['windows'] ) ) {
515 return $fallback;
516 }
517
518 foreach ( $session['windows'] as $win ) {
519 if ( ! isset( $win['id'], $win['url'] ) ) {
520 continue;
521 }
522 if ( $win['id'] !== $session['focused'] ) {
523 continue;
524 }
525 if ( ! openstation_url_is_same_admin( $win['url'] ) ) {
526 return $fallback;
527 }
528 return remove_query_arg( array( 'openstation_chromeless', OPENSTATION_PORTAL_FLAG ), $win['url'] );
529 }
530
531 return $fallback;
532 }
533
534 /**
535 * Validates and normalizes a `target` query arg on the portal URL.
536 *
537 * Accepts a raw request-URI-shaped string (path + optional query, e.g.
538 * `/wp-admin/profile.php?foo=bar`) and returns a fully-qualified admin
539 * URL if — and only if — it resolves to a same-origin `wp-admin/` path.
540 * Everything else returns an empty string so the caller falls back to
541 * the saved-session entry URL.
542 *
543 * Strips `openstation_chromeless` and the portal flag from the query so the target
544 * doesn't chain us into a chromeless standalone load or an infinite
545 * redirect loop.
546 *
547 * @param string $raw Raw value from `$_GET['target']` (already unslashed).
548 * @return string A safe absolute admin URL, or '' if the input is invalid.
549 */
550 function openstation_sanitize_portal_target( $raw ) {
551 if ( ! is_string( $raw ) || '' === $raw ) {
552 return '';
553 }
554
555 // Reject URIs with a scheme or protocol-relative prefix — we only
556 // accept relative paths so there's no way to redirect off-site.
557 if ( preg_match( '#^([a-z][a-z0-9+.-]*:|//)#i', $raw ) ) {
558 return '';
559 }
560
561 // Must be an absolute path starting with /.
562 if ( '/' !== $raw[0] ) {
563 return '';
564 }
565
566 $path = wp_parse_url( $raw, PHP_URL_PATH );
567 $query = wp_parse_url( $raw, PHP_URL_QUERY );
568 if ( ! is_string( $path ) || '' === $path ) {
569 return '';
570 }
571
572 $admin_path = wp_parse_url( admin_url(), PHP_URL_PATH );
573 $admin_path = is_string( $admin_path ) ? $admin_path : '/wp-admin/';
574 if ( 0 !== strpos( $path, $admin_path ) ) {
575 return '';
576 }
577
578 $file = substr( $path, strlen( $admin_path ) );
579 $file = ltrim( (string) $file, '/' );
580 if ( '' === $file ) {
581 $file = 'index.php';
582 }
583
584 // Resolve against the hardcoded allowlist of canonical wp-admin
585 // filenames (see `openstation_admin_target_allowlist()`). A
586 // regex alone would accept a plausible-looking filename that
587 // isn't a real core admin page (e.g. `custom_admin_page.php`)
588 // and effectively become an open redirect to a 404 page served
589 // under the admin path; the explicit allowlist closes that.
590 $target = openstation_resolve_admin_target( $file );
591 if ( is_wp_error( $target ) ) {
592 return '';
593 }
594
595 if ( is_string( $query ) && '' !== $query ) {
596 parse_str( $query, $args );
597 unset( $args['openstation_chromeless'], $args[ OPENSTATION_PORTAL_FLAG ], $args[ OPENSTATION_PORTAL_INTENT_FLAG ], $args['target'] );
598 if ( ! empty( $args ) ) {
599 $target = add_query_arg( $args, $target );
600 }
601 }
602
603 return $target;
604 }
605