| @@ -24,8 +24,79 @@ | ||
| 24 | 24 | * deliberate — it is NOT a half-finished rename. |
| 25 | 25 | */ |
| 26 | 26 | const OPENSTATION_SESSION_META_KEY = 'desktop_mode_session'; |
| 27 | 27 | |
| 28 | +/** | |
| 29 | + * The session meta key for the admin the request is running against. | |
| 30 | + * | |
| 31 | + * User meta is network-wide, so every site shared one blob, and the | |
| 32 | + * sanitizer drops any window URL outside the current site's | |
| 33 | + * `admin_url()` — so the first save from site B rewrote it and site A's | |
| 34 | + * desktop was gone. The MAIN site keeps the bare key, and so does every | |
| 35 | + * single-site install: those sessions already exist, and a new key would | |
| 36 | + * silently empty every desktop on upgrade. | |
| 37 | + * | |
| 38 | + * The NETWORK admin gets its own key rather than sharing the main | |
| 39 | + * site's, even though it runs in the main site's blog context. The two | |
| 40 | + * desktops derive the same window ids from different admins — | |
| 41 | + * `index-php` is the site dashboard on one and the network dashboard on | |
| 42 | + * the other — so one shared blob meant the network desktop restored the | |
| 43 | + * site's dashboard window (and the reverse), and the dock's Dashboard | |
| 44 | + * tile focused the wrong admin's screen. | |
| 45 | + * | |
| 46 | + * @param bool|null $network The network admin's session (true) or the | |
| 47 | + * current site's (false). Null follows the | |
| 48 | + * request, which is wrong on `admin-ajax.php` | |
| 49 | + * and REST — those callers must pass what the | |
| 50 | + * client reported. | |
| 51 | + * @return string Meta key to read and write. | |
| 52 | + */ | |
| 53 | +function openstation_session_meta_key( $network = null ) { | |
| 54 | + if ( null === $network ) { | |
| 55 | + $network = is_multisite() && is_network_admin(); | |
| 56 | + } | |
| 57 | + if ( $network ) { | |
| 58 | + return OPENSTATION_SESSION_META_KEY . '_network'; | |
| 59 | + } | |
| 60 | + return ! is_multisite() || get_current_blog_id() === get_main_site_id() | |
| 61 | + ? OPENSTATION_SESSION_META_KEY | |
| 62 | + : OPENSTATION_SESSION_META_KEY . '_' . get_current_blog_id(); | |
| 63 | +} | |
| 64 | + | |
| 65 | +/** | |
| 66 | + * Whether a persisted window URL belongs to the session's admin. | |
| 67 | + * | |
| 68 | + * The scope gate behind the per-admin meta keys: keys separate the | |
| 69 | + * blobs, this separates their CONTENTS, so a blob written before the | |
| 70 | + * keys split (or by an older client posting to the wrong scope) heals | |
| 71 | + * on read instead of restoring one admin's window on the other's | |
| 72 | + * desktop. Runs on read and on sanitize both. | |
| 73 | + * | |
| 74 | + * @param string $url Absolute window URL, already same-admin checked. | |
| 75 | + * @param bool $network Whether the session is the network admin's. | |
| 76 | + * @return bool True when the URL lives in the session's own admin. | |
| 77 | + */ | |
| 78 | +function openstation_session_url_in_scope( $url, $network ) { | |
| 79 | + $path = wp_parse_url( $url, PHP_URL_PATH ); | |
| 80 | + $in_network = is_string( $path ) && false !== strpos( $path, '/wp-admin/network/' ); | |
| 81 | + return $in_network === (bool) $network; | |
| 82 | +} | |
| 83 | + | |
| 84 | +/** | |
| 85 | + * Whether a window URL may persist in this session: same-origin, under | |
| 86 | + * this site's admin path, on the right side of the network split. The | |
| 87 | + * rule every window has to satisfy — on a network every site is its | |
| 88 | + * own OpenStation, and one admin's window never persists into | |
| 89 | + * another's session. | |
| 90 | + * | |
| 91 | + * @param string $url Absolute window URL. | |
| 92 | + * @param bool $network Whether the session is the network admin's. | |
| 93 | + * @return bool | |
| 94 | + */ | |
| 95 | +function openstation_session_window_url_ok( $url, $network ) { | |
| 96 | + return openstation_url_is_same_admin( $url ) && openstation_session_url_in_scope( $url, $network ); | |
| 97 | +} | |
| 98 | + | |
| 28 | 99 | /** Hard cap on persisted windows — guards against runaway meta size. */ |
| 29 | 100 | const OPENSTATION_SESSION_MAX_WINDOWS = 32; |
| 30 | 101 | |
| 31 | 102 | /** |
| @@ -90,18 +161,22 @@ | ||
| 90 | 161 | * |
| 91 | 162 | * Always returns a well-shaped array so callers don't have to defend |
| 92 | 163 | * against corrupt or partial meta. |
| 93 | 164 | * |
| 94 | - * @param int $user_id The user ID. | |
| 165 | + * @param int $user_id The user ID. | |
| 166 | + * @param bool|null $network See {@see openstation_session_meta_key()}. | |
| 95 | 167 | * @return array{windows: array, desktops: array, activeDesktop: string, focused: string, updated: int} |
| 96 | 168 | */ |
| 97 | -function openstation_get_session( $user_id ) { | |
| 169 | +function openstation_get_session( $user_id, $network = null ) { | |
| 98 | 170 | $user_id = (int) $user_id; |
| 99 | 171 | if ( $user_id <= 0 ) { |
| 100 | 172 | return openstation_empty_session(); |
| 101 | 173 | } |
| 174 | + if ( null === $network ) { | |
| 175 | + $network = is_multisite() && is_network_admin(); | |
| 176 | + } | |
| 102 | 177 | |
| 103 | - $raw = get_user_meta( $user_id, OPENSTATION_SESSION_META_KEY, true ); | |
| 178 | + $raw = get_user_meta( $user_id, openstation_session_meta_key( $network ), true ); | |
| 104 | 179 | if ( ! is_array( $raw ) ) { |
| 105 | 180 | return openstation_empty_session(); |
| 106 | 181 | } |
| 107 | 182 | |
| @@ -113,10 +188,54 @@ | ||
| 113 | 188 | ? array_values( $raw['desktops'] ) |
| 114 | 189 | : array( openstation_default_desktop() ); |
| 115 | 190 | $active_desktop = isset( $raw['activeDesktop'] ) ? (string) $raw['activeDesktop'] : 'desktop-1'; |
| 116 | 191 | |
| 192 | + // A desktop persisted by the site-Spaces model carried a `scope`: a | |
| 193 | + // desk hosting another admin. Every site is its own OpenStation now, | |
| 194 | + // so such a desk has nothing left to host — dropped on read, with | |
| 195 | + // the active desktop moved off it, and the next save writes the | |
| 196 | + // blob without it. | |
| 197 | + $desktops = array_values( | |
| 198 | + array_filter( | |
| 199 | + $desktops, | |
| 200 | + static function ( $d ) { | |
| 201 | + return ! ( is_array( $d ) && isset( $d['scope'] ) ); | |
| 202 | + } | |
| 203 | + ) | |
| 204 | + ); | |
| 205 | + if ( empty( $desktops ) ) { | |
| 206 | + $desktops = array( openstation_default_desktop() ); | |
| 207 | + } | |
| 208 | + $desktop_ids = array(); | |
| 209 | + foreach ( $desktops as $d ) { | |
| 210 | + if ( is_array( $d ) && isset( $d['id'] ) ) { | |
| 211 | + $desktop_ids[] = (string) $d['id']; | |
| 212 | + } | |
| 213 | + } | |
| 214 | + if ( ! in_array( $active_desktop, $desktop_ids, true ) && ! empty( $desktop_ids ) ) { | |
| 215 | + $active_desktop = $desktop_ids[0]; | |
| 216 | + } | |
| 217 | + | |
| 218 | + // Scope gate on read: drop windows persisted for the other admin. | |
| 219 | + // Blobs written before the network admin had its own meta key mix | |
| 220 | + // the two, and restoring across the split would open one admin's | |
| 221 | + // window on the other's desktop under a colliding window id. | |
| 222 | + $windows = isset( $raw['windows'] ) && is_array( $raw['windows'] ) ? array_values( $raw['windows'] ) : array(); | |
| 223 | + $windows = array_values( | |
| 224 | + array_filter( | |
| 225 | + $windows, | |
| 226 | + static function ( $win ) use ( $network ) { | |
| 227 | + if ( ! is_array( $win ) || ! empty( $win['native'] ) ) { | |
| 228 | + return true; | |
| 229 | + } | |
| 230 | + $url = isset( $win['url'] ) ? (string) $win['url'] : ''; | |
| 231 | + return openstation_session_window_url_ok( $url, $network ); | |
| 232 | + } | |
| 233 | + ) | |
| 234 | + ); | |
| 235 | + | |
| 117 | 236 | return array( |
| 118 | - 'windows' => isset( $raw['windows'] ) && is_array( $raw['windows'] ) ? array_values( $raw['windows'] ) : array(), | |
| 237 | + 'windows' => $windows, | |
| 119 | 238 | 'desktops' => $desktops, |
| 120 | 239 | 'activeDesktop' => $active_desktop, |
| 121 | 240 | 'focused' => isset( $raw['focused'] ) ? (string) $raw['focused'] : '', |
| 122 | 241 | 'updated' => isset( $raw['updated'] ) ? (int) $raw['updated'] : 0, |
| @@ -148,22 +267,26 @@ | ||
| 148 | 267 | * |
| 149 | 268 | * Equal timestamps are still accepted — that's a tie and whichever the |
| 150 | 269 | * server processes first wins. |
| 151 | 270 | * |
| 152 | - * @param int $user_id The user ID. | |
| 153 | - * @param array $session Raw session payload (will be sanitized). | |
| 271 | + * @param int $user_id The user ID. | |
| 272 | + * @param array $session Raw session payload (will be sanitized). | |
| 273 | + * @param bool|null $network See {@see openstation_session_meta_key()}. | |
| 154 | 274 | * @return bool True on success, false when stale / invalid / failed. |
| 155 | 275 | */ |
| 156 | -function openstation_save_session( $user_id, $session ) { | |
| 276 | +function openstation_save_session( $user_id, $session, $network = null ) { | |
| 157 | 277 | $user_id = (int) $user_id; |
| 158 | 278 | if ( $user_id <= 0 ) { |
| 159 | 279 | return false; |
| 160 | 280 | } |
| 281 | + if ( null === $network ) { | |
| 282 | + $network = is_multisite() && is_network_admin(); | |
| 283 | + } | |
| 161 | 284 | |
| 162 | 285 | if ( is_array( $session ) && isset( $session['updated'] ) ) { |
| 163 | 286 | $incoming = (int) $session['updated']; |
| 164 | 287 | if ( $incoming > 0 ) { |
| 165 | - $existing = openstation_get_session( $user_id ); | |
| 288 | + $existing = openstation_get_session( $user_id, $network ); | |
| 166 | 289 | $stored = isset( $existing['updated'] ) ? (int) $existing['updated'] : 0; |
| 167 | 290 | if ( $incoming < $stored ) { |
| 168 | 291 | // Stale write — another tab saved a newer snapshot |
| 169 | 292 | // after this one was taken. Bail so the user's latest |
| @@ -172,38 +295,44 @@ | ||
| 172 | 295 | } |
| 173 | 296 | } |
| 174 | 297 | } |
| 175 | 298 | |
| 176 | - $clean = openstation_sanitize_session( $session ); | |
| 299 | + $clean = openstation_sanitize_session( $session, $network ); | |
| 177 | 300 | |
| 178 | - return false !== update_user_meta( $user_id, OPENSTATION_SESSION_META_KEY, $clean ); | |
| 301 | + return false !== update_user_meta( $user_id, openstation_session_meta_key( $network ), $clean ); | |
| 179 | 302 | } |
| 180 | 303 | |
| 181 | 304 | /** |
| 182 | 305 | * Clears a user's saved desktop session. |
| 183 | 306 | * |
| 184 | - * @param int $user_id The user ID. | |
| 307 | + * @param int $user_id The user ID. | |
| 308 | + * @param bool|null $network See {@see openstation_session_meta_key()}. | |
| 185 | 309 | * @return bool True on success. |
| 186 | 310 | */ |
| 187 | -function openstation_clear_session( $user_id ) { | |
| 311 | +function openstation_clear_session( $user_id, $network = null ) { | |
| 188 | 312 | $user_id = (int) $user_id; |
| 189 | 313 | if ( $user_id <= 0 ) { |
| 190 | 314 | return false; |
| 191 | 315 | } |
| 192 | - return (bool) delete_user_meta( $user_id, OPENSTATION_SESSION_META_KEY ); | |
| 316 | + return (bool) delete_user_meta( $user_id, openstation_session_meta_key( $network ) ); | |
| 193 | 317 | } |
| 194 | 318 | |
| 195 | 319 | /** |
| 196 | 320 | * Sanitizes a session payload before persistence. |
| 197 | 321 | * |
| 198 | - * Rejects windows whose `url` isn't a same-origin admin URL, clamps | |
| 199 | - * geometry to sane integer ranges, and normalizes the state enum. | |
| 200 | - * Windows beyond {@see OPENSTATION_SESSION_MAX_WINDOWS} are dropped. | |
| 322 | + * Rejects windows whose `url` isn't a same-origin admin URL or lives | |
| 323 | + * in the other admin's scope, clamps geometry to sane integer ranges, | |
| 324 | + * and normalizes the state enum. Windows beyond | |
| 325 | + * {@see OPENSTATION_SESSION_MAX_WINDOWS} are dropped. | |
| 201 | 326 | * |
| 202 | - * @param mixed $session Raw session data from the client. | |
| 327 | + * @param mixed $session Raw session data from the client. | |
| 328 | + * @param bool|null $network See {@see openstation_session_meta_key()}. | |
| 203 | 329 | * @return array{windows: array, desktops: array, activeDesktop: string, focused: string, updated: int} |
| 204 | 330 | */ |
| 205 | -function openstation_sanitize_session( $session ) { | |
| 331 | +function openstation_sanitize_session( $session, $network = null ) { | |
| 332 | + if ( null === $network ) { | |
| 333 | + $network = is_multisite() && is_network_admin(); | |
| 334 | + } | |
| 206 | 335 | $clean = openstation_empty_session(); |
| 207 | 336 | |
| 208 | 337 | if ( ! is_array( $session ) ) { |
| 209 | 338 | $clean['updated'] = openstation_session_now_ms(); |
| @@ -248,12 +377,21 @@ | ||
| 248 | 377 | // human-typed desktop name, hard ceiling on meta size. |
| 249 | 378 | if ( strlen( $d_label ) > 64 ) { |
| 250 | 379 | $d_label = substr( $d_label, 0, 64 ); |
| 251 | 380 | } |
| 252 | - $clean_desktops[] = array( | |
| 381 | + $entry = array( | |
| 253 | 382 | 'id' => $d_id, |
| 254 | 383 | 'label' => $d_label, |
| 255 | 384 | ); |
| 385 | + // A desktop's workspace profile — which apps it shows, what | |
| 386 | + // it opens with, how they are arranged. Optional, and only | |
| 387 | + // written when there is one, so a plain Space keeps the | |
| 388 | + // shape every session saved before workspaces existed had. | |
| 389 | + $profile = openstation_sanitize_workspace_profile( isset( $d['profile'] ) ? $d['profile'] : null ); | |
| 390 | + if ( null !== $profile ) { | |
| 391 | + $entry['profile'] = $profile; | |
| 392 | + } | |
| 393 | + $clean_desktops[] = $entry; | |
| 256 | 394 | $desktop_ids[] = $d_id; |
| 257 | 395 | if ( count( $clean_desktops ) >= OPENSTATION_SESSION_MAX_DESKTOPS ) { |
| 258 | 396 | break; |
| 259 | 397 | } |
| @@ -319,8 +457,18 @@ | ||
| 319 | 457 | if ( '' === $base_id ) { |
| 320 | 458 | $base_id = $id; |
| 321 | 459 | } |
| 322 | 460 | |
| 461 | + // Map the window to a known desktop. A client that sends a | |
| 462 | + // desktopId pointing at a non-existent desktop (race with a | |
| 463 | + // desktop close, or a malicious payload) is silently | |
| 464 | + // remapped to the active desktop so the window remains | |
| 465 | + // visible — losing it on restore would be the worse UX. | |
| 466 | + $win_desktop = isset( $win['desktopId'] ) ? sanitize_key( (string) $win['desktopId'] ) : ''; | |
| 467 | + if ( '' === $win_desktop || ! in_array( $win_desktop, $desktop_ids, true ) ) { | |
| 468 | + $win_desktop = $clean['activeDesktop']; | |
| 469 | + } | |
| 470 | + | |
| 323 | 471 | // Native windows (OS Settings, Bug Report, anything from |
| 324 | 472 | // `openstation_register_window()`) carry no admin URL — |
| 325 | 473 | // the shell reconstructs them from the registry by id. Their |
| 326 | 474 | // `url` is a `#slug` marker, which would fail the same-admin |
| @@ -334,14 +482,14 @@ | ||
| 334 | 482 | if ( $is_native ) { |
| 335 | 483 | $url = '#' . $id; |
| 336 | 484 | } else { |
| 337 | 485 | $url = isset( $win['url'] ) ? esc_url_raw( (string) $win['url'] ) : ''; |
| 338 | - // Only allow URLs that land inside our own wp-admin — both | |
| 339 | - // a safety net against storing arbitrary origins in user meta | |
| 340 | - // and a guarantee the restore path won't try to iframe a | |
| 341 | - // cross-origin page. Host+path parsing rejects tricks like | |
| 342 | - // `//evil.com/wp-admin/…` that a raw prefix check would miss. | |
| 343 | - if ( '' === $url || ! openstation_url_is_same_admin( $url ) ) { | |
| 486 | + // Only allow URLs this session may hold: same-origin, | |
| 487 | + // inside this admin. Host+path parsing rejects tricks | |
| 488 | + // like `//evil.com/wp-admin/…` that a raw prefix check | |
| 489 | + // would miss, and one admin's window never persists | |
| 490 | + // into another's session. | |
| 491 | + if ( '' === $url || ! openstation_session_window_url_ok( $url, $network ) ) { | |
| 344 | 492 | continue; |
| 345 | 493 | } |
| 346 | 494 | // Strip transient/routing flags before storage. The chromeless |
| 347 | 495 | // `openstation_chromeless` flag is an iframe-only concern and must never |
| @@ -357,18 +505,8 @@ | ||
| 357 | 505 | if ( ! in_array( $state, OPENSTATION_SESSION_STATES, true ) ) { |
| 358 | 506 | $state = 'normal'; |
| 359 | 507 | } |
| 360 | 508 | |
| 361 | - // Map the window to a known desktop. A client that sends a | |
| 362 | - // desktopId pointing at a non-existent desktop (race with | |
| 363 | - // a desktop close, or a malicious payload) is silently | |
| 364 | - // remapped to the active desktop so the window remains | |
| 365 | - // visible — losing it on restore would be the worse UX. | |
| 366 | - $win_desktop = isset( $win['desktopId'] ) ? sanitize_key( (string) $win['desktopId'] ) : ''; | |
| 367 | - if ( '' === $win_desktop || ! in_array( $win_desktop, $desktop_ids, true ) ) { | |
| 368 | - $win_desktop = $clean['activeDesktop']; | |
| 369 | - } | |
| 370 | - | |
| 371 | 509 | $entry = array( |
| 372 | 510 | 'id' => $id, |
| 373 | 511 | 'baseId' => $base_id, |
| 374 | 512 | 'desktopId' => $win_desktop, |
| @@ -381,8 +519,26 @@ | ||
| 381 | 519 | 'width' => openstation_sanitize_session_dimension( $win['width'] ?? 800, 0, 20000 ), |
| 382 | 520 | 'height' => openstation_sanitize_session_dimension( $win['height'] ?? 600, 0, 20000 ), |
| 383 | 521 | ); |
| 384 | 522 | |
| 523 | + // A grid-snapped window's cells, next to its pixels. On | |
| 524 | + // restore the cells win — they are a fraction of the desk, | |
| 525 | + // and the pixels are from whatever display the session was | |
| 526 | + // saved on. Only written when valid, so a plain window keeps | |
| 527 | + // the shape it always had. | |
| 528 | + $grid_span = openstation_sanitize_session_grid_span( $win['gridSpan'] ?? null ); | |
| 529 | + if ( null !== $grid_span ) { | |
| 530 | + $entry['gridSpan'] = $grid_span; | |
| 531 | + } | |
| 532 | + | |
| 533 | + // A window the phone layer opened with no desktop geometry to | |
| 534 | + // keep: its pixels are a phone's defaults, and the shell's | |
| 535 | + // restore path places it afresh instead of trusting them. | |
| 536 | + // Only written when true so plain sessions keep their shape. | |
| 537 | + if ( ! empty( $win['unplaced'] ) ) { | |
| 538 | + $entry['unplaced'] = true; | |
| 539 | + } | |
| 540 | + | |
| 385 | 541 | // Marks the entry for the shell's restore path: native |
| 386 | 542 | // windows reopen through the native-window registry, not by |
| 387 | 543 | // pointing an iframe at a URL. Only written when true so |
| 388 | 544 | // sessions of plain admin windows keep their existing shape. |
| @@ -500,8 +656,59 @@ | ||
| 500 | 656 | return $value; |
| 501 | 657 | } |
| 502 | 658 | |
| 503 | 659 | /** |
| 660 | + * Sanitize a window's grid placement. | |
| 661 | + * | |
| 662 | + * `{ anchor: { col, row }, cursor: { col, row }, cols, rows }`, every | |
| 663 | + * value an integer, the grid between 1×1 and 24×24 (the same ceiling | |
| 664 | + * the client's dimensions filter enforces), every cell inside it. | |
| 665 | + * Anything else is `null` — the window restores on its pixels, which | |
| 666 | + * is what a session written before grid snap does anyway. | |
| 667 | + * | |
| 668 | + * @param mixed $raw Raw span from the payload. | |
| 669 | + * @return array|null Sanitized span, or null. | |
| 670 | + */ | |
| 671 | +function openstation_sanitize_session_grid_span( $raw ) { | |
| 672 | + if ( ! is_array( $raw ) || ! isset( $raw['anchor'], $raw['cursor'], $raw['cols'], $raw['rows'] ) ) { | |
| 673 | + return null; | |
| 674 | + } | |
| 675 | + $int = static function ( $value ) { | |
| 676 | + return is_int( $value ) || ( is_numeric( $value ) && (string) (int) $value === (string) $value ) ? (int) $value : null; | |
| 677 | + }; | |
| 678 | + $cols = $int( $raw['cols'] ); | |
| 679 | + $rows = $int( $raw['rows'] ); | |
| 680 | + if ( null === $cols || null === $rows || $cols < 1 || $rows < 1 || $cols > 24 || $rows > 24 ) { | |
| 681 | + return null; | |
| 682 | + } | |
| 683 | + $cell = static function ( $c ) use ( $int, $cols, $rows ) { | |
| 684 | + if ( ! is_array( $c ) || ! isset( $c['col'], $c['row'] ) ) { | |
| 685 | + return null; | |
| 686 | + } | |
| 687 | + $col = $int( $c['col'] ); | |
| 688 | + $row = $int( $c['row'] ); | |
| 689 | + if ( null === $col || null === $row || $col < 0 || $row < 0 || $col >= $cols || $row >= $rows ) { | |
| 690 | + return null; | |
| 691 | + } | |
| 692 | + return array( | |
| 693 | + 'col' => $col, | |
| 694 | + 'row' => $row, | |
| 695 | + ); | |
| 696 | + }; | |
| 697 | + $anchor = $cell( $raw['anchor'] ); | |
| 698 | + $cursor = $cell( $raw['cursor'] ); | |
| 699 | + if ( null === $anchor || null === $cursor ) { | |
| 700 | + return null; | |
| 701 | + } | |
| 702 | + return array( | |
| 703 | + 'anchor' => $anchor, | |
| 704 | + 'cursor' => $cursor, | |
| 705 | + 'cols' => $cols, | |
| 706 | + 'rows' => $rows, | |
| 707 | + ); | |
| 708 | +} | |
| 709 | + | |
| 710 | +/** | |
| 504 | 711 | * Sanitize a native window's open-time params. |
| 505 | 712 | * |
| 506 | 713 | * These say WHAT a native window is showing (`{ userId: 12 }`, |
| 507 | 714 | * `{ customerId: 7 }`) as opposed to what it is — see |
| @@ -564,8 +771,18 @@ | ||
| 564 | 771 | * Registers the REST routes used by the desktop shell to load and save |
| 565 | 772 | * the current user's session. |
| 566 | 773 | */ |
| 567 | 774 | function openstation_register_session_rest_routes() { |
| 775 | + // `network` addresses the network admin's own session. The route | |
| 776 | + // runs in the main site's blog context whichever desktop is | |
| 777 | + // saving, so the shell says which one it is: the network screen's | |
| 778 | + // `sessionUrl` carries `network=1`. | |
| 779 | + $network_arg = array( | |
| 780 | + 'network' => array( | |
| 781 | + 'type' => 'boolean', | |
| 782 | + 'default' => false, | |
| 783 | + ), | |
| 784 | + ); | |
| 568 | 785 | register_rest_route( |
| 569 | 786 | 'desktop-mode/v1', |
| 570 | 787 | '/session', |
| 571 | 788 | array( |
| @@ -572,18 +789,22 @@ | ||
| 572 | 789 | array( |
| 573 | 790 | 'methods' => WP_REST_Server::READABLE, |
| 574 | 791 | 'callback' => 'openstation_rest_get_session', |
| 575 | 792 | 'permission_callback' => 'openstation_rest_session_permission', |
| 793 | + 'args' => $network_arg, | |
| 576 | 794 | ), |
| 577 | 795 | array( |
| 578 | 796 | 'methods' => WP_REST_Server::CREATABLE, |
| 579 | 797 | 'callback' => 'openstation_rest_save_session', |
| 580 | 798 | 'permission_callback' => 'openstation_rest_session_permission', |
| 581 | - 'args' => array( | |
| 582 | - 'session' => array( | |
| 583 | - 'required' => true, | |
| 584 | - 'type' => 'object', | |
| 799 | + 'args' => array_merge( | |
| 800 | + array( | |
| 801 | + 'session' => array( | |
| 802 | + 'required' => true, | |
| 803 | + 'type' => 'object', | |
| 804 | + ), | |
| 585 | 805 | ), |
| 806 | + $network_arg | |
| 586 | 807 | ), |
| 587 | 808 | ), |
| 588 | 809 | array( |
| 589 | 810 | 'methods' => WP_REST_Server::DELETABLE, |
| @@ -588,8 +809,9 @@ | ||
| 588 | 809 | array( |
| 589 | 810 | 'methods' => WP_REST_Server::DELETABLE, |
| 590 | 811 | 'callback' => 'openstation_rest_clear_session', |
| 591 | 812 | 'permission_callback' => 'openstation_rest_session_permission', |
| 813 | + 'args' => $network_arg, | |
| 592 | 814 | ), |
| 593 | 815 | ) |
| 594 | 816 | ); |
| 595 | 817 | } |
| @@ -606,14 +828,35 @@ | ||
| 606 | 828 | return openstation_rest_require_enabled(); |
| 607 | 829 | } |
| 608 | 830 | |
| 609 | 831 | /** |
| 832 | + * Which admin's session a REST call addresses. | |
| 833 | + * | |
| 834 | + * `is_network_admin()` is false on every REST request, so the client | |
| 835 | + * reports the scope (`network=1`, stamped onto the network screen's | |
| 836 | + * `sessionUrl`). Honoured only for users who can open the network | |
| 837 | + * desktop at all — anyone else's flag falls back to the site session | |
| 838 | + * rather than minting a blob for a desktop they cannot reach. | |
| 839 | + * | |
| 840 | + * @param WP_REST_Request $request The REST request. | |
| 841 | + * @return bool True when the call addresses the network admin's session. | |
| 842 | + */ | |
| 843 | +function openstation_rest_session_network( WP_REST_Request $request ) { | |
| 844 | + return is_multisite() | |
| 845 | + && rest_sanitize_boolean( $request->get_param( 'network' ) ) | |
| 846 | + && current_user_can( 'manage_network' ); | |
| 847 | +} | |
| 848 | + | |
| 849 | +/** | |
| 610 | 850 | * GET /desktop-mode/v1/session — returns the caller's session. |
| 611 | 851 | * |
| 852 | + * @param WP_REST_Request $request The REST request. | |
| 612 | 853 | * @return WP_REST_Response |
| 613 | 854 | */ |
| 614 | -function openstation_rest_get_session() { | |
| 615 | - return rest_ensure_response( openstation_get_session( get_current_user_id() ) ); | |
| 855 | +function openstation_rest_get_session( WP_REST_Request $request ) { | |
| 856 | + return rest_ensure_response( | |
| 857 | + openstation_get_session( get_current_user_id(), openstation_rest_session_network( $request ) ) | |
| 858 | + ); | |
| 616 | 859 | } |
| 617 | 860 | |
| 618 | 861 | /** |
| 619 | 862 | * POST /desktop-mode/v1/session — replaces the caller's session. |
| @@ -623,17 +866,19 @@ | ||
| 623 | 866 | */ |
| 624 | 867 | function openstation_rest_save_session( WP_REST_Request $request ) { |
| 625 | 868 | $user_id = get_current_user_id(); |
| 626 | 869 | $payload = $request->get_param( 'session' ); |
| 627 | - openstation_save_session( $user_id, $payload ); | |
| 628 | - return rest_ensure_response( openstation_get_session( $user_id ) ); | |
| 870 | + $network = openstation_rest_session_network( $request ); | |
| 871 | + openstation_save_session( $user_id, $payload, $network ); | |
| 872 | + return rest_ensure_response( openstation_get_session( $user_id, $network ) ); | |
| 629 | 873 | } |
| 630 | 874 | |
| 631 | 875 | /** |
| 632 | 876 | * DELETE /desktop-mode/v1/session — clears the caller's session. |
| 633 | 877 | * |
| 878 | + * @param WP_REST_Request $request The REST request. | |
| 634 | 879 | * @return WP_REST_Response |
| 635 | 880 | */ |
| 636 | -function openstation_rest_clear_session() { | |
| 637 | - openstation_clear_session( get_current_user_id() ); | |
| 881 | +function openstation_rest_clear_session( WP_REST_Request $request ) { | |
| 882 | + openstation_clear_session( get_current_user_id(), openstation_rest_session_network( $request ) ); | |
| 638 | 883 | return rest_ensure_response( openstation_empty_session() ); |
| 639 | 884 | } |