| @@ -149,8 +149,11 @@ | ||
| 149 | 149 | // Site). Off-origin hits still redirect into admin so shared |
| 150 | 150 | // links keep working; they just don't silently mutate user-meta. |
| 151 | 151 | if ( $auto_enable && openstation_portal_is_same_origin_navigation() && '1' !== get_user_meta( $user_id, 'desktop_mode_mode', true ) ) { |
| 152 | 152 | update_user_meta( $user_id, 'desktop_mode_mode', '1' ); |
| 153 | + // Same stamps + action as the AJAX toggle; the portal is the | |
| 154 | + // second of the two paths that turn a user on. | |
| 155 | + openstation_record_user_enabled( $user_id ); | |
| 153 | 156 | } |
| 154 | 157 | |
| 155 | 158 | // Pick the page the shell opens first. An explicit `target` query |
| 156 | 159 | // arg — a same-origin wp-admin URL — is how |
| @@ -295,12 +298,14 @@ | ||
| 295 | 298 | * intent flag was present. The flags stay frozen (see AGENTS.md); |
| 296 | 299 | * only what they resolve to moved. |
| 297 | 300 | * |
| 298 | 301 | * 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. The shell | |
| 301 | - * screen itself, chromeless loads, solo boots and classic-flagged | |
| 302 | - * requests pass through. | |
| 302 | + * — AJAX, REST, cron, admin-post.php, non-GET methods, and sub-resource | |
| 303 | + * fetches (an `<img>`, a script or an XHR whose URL is an admin page, | |
| 304 | + * see {@see openstation_is_subresource_request()}) — so the hook can't | |
| 305 | + * corrupt a form submission, break an API call or hand an image tag an | |
| 306 | + * HTML document. The shell screen itself, chromeless loads, solo boots | |
| 307 | + * and classic-flagged requests pass through. | |
| 303 | 308 | * |
| 304 | 309 | * Disable via the `openstation_admin_redirect_to_portal` filter (return |
| 305 | 310 | * false); plain admin pages then render as classic admin and the |
| 306 | 311 | * desktop lives at `/openstation/` only. The alias route runs before |
| @@ -322,8 +327,16 @@ | ||
| 322 | 327 | // A solo boot renders one window in place, wherever it landed. |
| 323 | 328 | if ( function_exists( 'openstation_is_solo_request' ) && openstation_is_solo_request() ) { |
| 324 | 329 | return; |
| 325 | 330 | } |
| 331 | + // The user admin (`wp-admin/user/`, multisite's dashboard for users | |
| 332 | + // with no site role) renders classic. It has no shell screen of its | |
| 333 | + // own, and its URLs never survive the target allowlist — before | |
| 334 | + // this pass-through the redirect claimed the request anyway and | |
| 335 | + // silently forwarded the user to the site desktop's default entry. | |
| 336 | + if ( is_multisite() && is_user_admin() ) { | |
| 337 | + return; | |
| 338 | + } | |
| 326 | 339 | if ( wp_doing_ajax() || wp_doing_cron() ) { |
| 327 | 340 | return; |
| 328 | 341 | } |
| 329 | 342 | if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { |
| @@ -331,8 +344,16 @@ | ||
| 331 | 344 | } |
| 332 | 345 | if ( ! empty( $_SERVER['REQUEST_METHOD'] ) && 'GET' !== strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) ) { |
| 333 | 346 | return; |
| 334 | 347 | } |
| 348 | + // The browser says what it is fetching for. An <img>, a script or | |
| 349 | + // an XHR aimed at an admin URL (Jetpack's admin-bar sparkline is | |
| 350 | + // admin.php?page=stats&noheader&proxy&chart=…) is not a user | |
| 351 | + // landing on a plain admin page, and forwarding it into the desktop | |
| 352 | + // only swaps the bytes it asked for with the shell's HTML. | |
| 353 | + if ( openstation_is_subresource_request() ) { | |
| 354 | + return; | |
| 355 | + } | |
| 335 | 356 | |
| 336 | 357 | // The "Detach to new tab" button tags its URL with this flag so the |
| 337 | 358 | // user can view one admin page classically without disabling desktop |
| 338 | 359 | // mode account-wide. Only affects the single request — subsequent |
| @@ -601,8 +622,18 @@ | ||
| 601 | 622 | } |
| 602 | 623 | |
| 603 | 624 | $file = substr( $path, strlen( $admin_path ) ); |
| 604 | 625 | $file = ltrim( (string) $file, '/' ); |
| 626 | + | |
| 627 | + // The network admin's own screens live one directory down and are | |
| 628 | + // resolved against their own list. Without this a network URL came | |
| 629 | + // back empty and the user was quietly forwarded to the site | |
| 630 | + // dashboard, which is a different admin. | |
| 631 | + $network = 0 === strpos( $file, 'network/' ); | |
| 632 | + if ( $network ) { | |
| 633 | + $file = substr( $file, strlen( 'network/' ) ); | |
| 634 | + } | |
| 635 | + | |
| 605 | 636 | if ( '' === $file ) { |
| 606 | 637 | $file = 'index.php'; |
| 607 | 638 | } |
| 608 | 639 | |
| @@ -611,9 +642,9 @@ | ||
| 611 | 642 | // regex alone would accept a plausible-looking filename that |
| 612 | 643 | // isn't a real core admin page (e.g. `custom_admin_page.php`) |
| 613 | 644 | // and effectively become an open redirect to a 404 page served |
| 614 | 645 | // under the admin path; the explicit allowlist closes that. |
| 615 | - $target = openstation_resolve_admin_target( $file ); | |
| 646 | + $target = openstation_resolve_admin_target( $file, $network ); | |
| 616 | 647 | if ( is_wp_error( $target ) ) { |
| 617 | 648 | return ''; |
| 618 | 649 | } |
| 619 | 650 | |
| @@ -628,8 +659,21 @@ | ||
| 628 | 659 | // The shell screen is where a target is opened, never a target: the |
| 629 | 660 | // shell would open itself in a window, and a redirect chain built |
| 630 | 661 | // from it would loop. Fall back to the entry resolver instead. |
| 631 | 662 | if ( openstation_url_is_shell_screen( $target ) ) { |
| 663 | + return ''; | |
| 664 | + } | |
| 665 | + | |
| 666 | + // `admin.php` is a bootstrap, not a page. Without a `page` arg core | |
| 667 | + // falls through the last `else` in `wp-admin/admin.php`, never | |
| 668 | + // requires `admin-header.php`, and answers 200 with an empty body — | |
| 669 | + // so the URL becomes a window showing nothing. The allowlist above | |
| 670 | + // matches filenames and cannot see the query, which is why the | |
| 671 | + // check belongs here, beside the shell-screen one: both are URLs | |
| 672 | + // that resolve but must not become a target. Returning '' hands the | |
| 673 | + // caller back to the entry resolver (session's focused window, else | |
| 674 | + // the default window, else the Dashboard). | |
| 675 | + if ( openstation_url_is_page_less_admin_php( $target ) ) { | |
| 632 | 676 | return ''; |
| 633 | 677 | } |
| 634 | 678 | |
| 635 | 679 | return $target; |