PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.0.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.0.1
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 / session.php

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

640 lines 22.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Session Persistence.
4 *
5 * Persists each user's open desktop windows — URLs, positions, sizes,
6 * states, and which window was focused — to user meta so a session can
7 * be restored across page loads and, via the `/openstation` portal,
8 * across devices. Cross-device viewport adaptation (a window that sat
9 * in the far-right corner of a 3440px ultrawide landing sanely on a
10 * 1280px laptop) happens client-side on restore.
11 *
12 * @package OpenStation
13 */
14
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * User meta key holding the serialized desktop session.
19 *
20 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
21 * persisted or externally-visible identifier, so renaming it would
22 * orphan data already written by live installs (or break a live
23 * URL). The mismatch between this constant's name and its value is
24 * deliberate — it is NOT a half-finished rename.
25 */
26 const OPENSTATION_SESSION_META_KEY = 'desktop_mode_session';
27
28 /** Hard cap on persisted windows — guards against runaway meta size. */
29 const OPENSTATION_SESSION_MAX_WINDOWS = 32;
30
31 /**
32 * Hard cap on a native window's persisted open-time params. These are
33 * "which user / which customer / which tab" — a handful of scalars,
34 * never a payload. The cap is what stops a careless (or hostile)
35 * client turning the session blob into a data store.
36 */
37 const OPENSTATION_SESSION_MAX_PARAMS = 12;
38
39 /** Hard cap on persisted desktops ("Spaces"). Generous — power-users
40 * with 8+ desktops are vanishingly rare, and we'd rather drop tail
41 * desktops than balloon user meta. */
42 const OPENSTATION_SESSION_MAX_DESKTOPS = 16;
43
44 /** Allowed values for a window's state field. */
45 const OPENSTATION_SESSION_STATES = array( 'normal', 'minimized', 'maximized', 'fullscreen' );
46
47 /**
48 * Current time as epoch milliseconds.
49 *
50 * The session's `updated` field is the ordering key for the
51 * stale-write guard and the client stamps it with `Date.now()`.
52 * Server-side fallbacks have to speak the same unit — see
53 * {@see openstation_save_session()} for why the resolution matters.
54 *
55 * @return int Epoch milliseconds.
56 */
57 function openstation_session_now_ms() {
58 return (int) round( microtime( true ) * 1000 );
59 }
60
61 /** Default desktop entry seeded into empty / corrupt sessions. */
62 function openstation_default_desktop() {
63 return array(
64 'id' => 'desktop-1',
65 'label' => 'Desktop 1',
66 );
67 }
68
69 /**
70 * Returns the default empty session shape.
71 *
72 * Includes a default desktop ("Desktop 1") so the client can always
73 * assume at least one desktop exists at boot — the shell can't
74 * function with zero desktops.
75 *
76 * @return array{windows: array, desktops: array, activeDesktop: string, focused: string, updated: int}
77 */
78 function openstation_empty_session() {
79 return array(
80 'windows' => array(),
81 'desktops' => array( openstation_default_desktop() ),
82 'activeDesktop' => 'desktop-1',
83 'focused' => '',
84 'updated' => 0,
85 );
86 }
87
88 /**
89 * Retrieves the saved desktop session for a user.
90 *
91 * Always returns a well-shaped array so callers don't have to defend
92 * against corrupt or partial meta.
93 *
94 * @param int $user_id The user ID.
95 * @return array{windows: array, desktops: array, activeDesktop: string, focused: string, updated: int}
96 */
97 function openstation_get_session( $user_id ) {
98 $user_id = (int) $user_id;
99 if ( $user_id <= 0 ) {
100 return openstation_empty_session();
101 }
102
103 $raw = get_user_meta( $user_id, OPENSTATION_SESSION_META_KEY, true );
104 if ( ! is_array( $raw ) ) {
105 return openstation_empty_session();
106 }
107
108 // Desktops + activeDesktop are post-0.4.0 additions. Sessions
109 // saved before they existed don't carry either field — fall back
110 // to the single default desktop so older sessions degrade
111 // gracefully rather than booting into a zero-desktop limbo.
112 $desktops = isset( $raw['desktops'] ) && is_array( $raw['desktops'] )
113 ? array_values( $raw['desktops'] )
114 : array( openstation_default_desktop() );
115 $active_desktop = isset( $raw['activeDesktop'] ) ? (string) $raw['activeDesktop'] : 'desktop-1';
116
117 return array(
118 'windows' => isset( $raw['windows'] ) && is_array( $raw['windows'] ) ? array_values( $raw['windows'] ) : array(),
119 'desktops' => $desktops,
120 'activeDesktop' => $active_desktop,
121 'focused' => isset( $raw['focused'] ) ? (string) $raw['focused'] : '',
122 'updated' => isset( $raw['updated'] ) ? (int) $raw['updated'] : 0,
123 );
124 }
125
126 /**
127 * Persists a sanitized desktop session to user meta.
128 *
129 * Rejects writes whose `updated` timestamp is older than what's
130 * already on file — a simple last-write-wins guard that prevents two
131 * tabs open on the same user from clobbering each other. The client
132 * stamps `updated` with `Date.now()` — epoch MILLISECONDS — at
133 * snapshot time (see `WindowManager.snapshot`), so this comparison
134 * lines up with real wall-clock ordering on same-machine multi-tab
135 * setups.
136 *
137 * Millisecond resolution is load-bearing, not cosmetic. The two
138 * writes that race hardest are a `keepalive` fetch still in flight
139 * and the `pagehide` beacon that supersedes it; at second resolution
140 * they tie, and the tie rule below hands the win to whichever the
141 * server processes last — which can be the stale one, reinstating a
142 * window the user just closed.
143 *
144 * Sessions written before the switch carry a seconds value. Those are
145 * ~1000x smaller than any millisecond stamp, so the first write after
146 * an upgrade always wins — which is the correct outcome for a stamp
147 * that is genuinely older.
148 *
149 * Equal timestamps are still accepted — that's a tie and whichever the
150 * server processes first wins.
151 *
152 * @param int $user_id The user ID.
153 * @param array $session Raw session payload (will be sanitized).
154 * @return bool True on success, false when stale / invalid / failed.
155 */
156 function openstation_save_session( $user_id, $session ) {
157 $user_id = (int) $user_id;
158 if ( $user_id <= 0 ) {
159 return false;
160 }
161
162 if ( is_array( $session ) && isset( $session['updated'] ) ) {
163 $incoming = (int) $session['updated'];
164 if ( $incoming > 0 ) {
165 $existing = openstation_get_session( $user_id );
166 $stored = isset( $existing['updated'] ) ? (int) $existing['updated'] : 0;
167 if ( $incoming < $stored ) {
168 // Stale write — another tab saved a newer snapshot
169 // after this one was taken. Bail so the user's latest
170 // work isn't overwritten by a slow-to-arrive payload.
171 return false;
172 }
173 }
174 }
175
176 $clean = openstation_sanitize_session( $session );
177
178 return false !== update_user_meta( $user_id, OPENSTATION_SESSION_META_KEY, $clean );
179 }
180
181 /**
182 * Clears a user's saved desktop session.
183 *
184 * @param int $user_id The user ID.
185 * @return bool True on success.
186 */
187 function openstation_clear_session( $user_id ) {
188 $user_id = (int) $user_id;
189 if ( $user_id <= 0 ) {
190 return false;
191 }
192 return (bool) delete_user_meta( $user_id, OPENSTATION_SESSION_META_KEY );
193 }
194
195 /**
196 * Sanitizes a session payload before persistence.
197 *
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.
201 *
202 * @param mixed $session Raw session data from the client.
203 * @return array{windows: array, desktops: array, activeDesktop: string, focused: string, updated: int}
204 */
205 function openstation_sanitize_session( $session ) {
206 $clean = openstation_empty_session();
207
208 if ( ! is_array( $session ) ) {
209 $clean['updated'] = openstation_session_now_ms();
210 return $clean;
211 }
212
213 // Preserve the client's `updated` timestamp so the stale-write guard
214 // in openstation_save_session compares client-to-client (not client-to-server
215 // wallclock) — two saves landing in the same millisecond must tie, not lose.
216 // The fallback matches the client's unit (epoch milliseconds); mixing
217 // units here would store a seconds value that every later comparison
218 // treats as ancient, quietly disabling the guard.
219 $incoming_updated = isset( $session['updated'] ) ? (int) $session['updated'] : 0;
220 $clean['updated'] = $incoming_updated > 0 ? $incoming_updated : openstation_session_now_ms();
221
222 if ( isset( $session['focused'] ) && is_string( $session['focused'] ) ) {
223 $clean['focused'] = sanitize_key( $session['focused'] );
224 }
225
226 // --- Desktops list -------------------------------------------
227 // Build a sanitized desktops array first so we can validate
228 // per-window desktopId against it below — windows assigned to
229 // non-existent desktops are quietly remapped to the active
230 // desktop on restore client-side, but we want server-side
231 // integrity too.
232 $desktop_ids = array();
233 if ( isset( $session['desktops'] ) && is_array( $session['desktops'] ) ) {
234 $clean_desktops = array();
235 foreach ( $session['desktops'] as $d ) {
236 if ( ! is_array( $d ) ) {
237 continue;
238 }
239 $d_id = isset( $d['id'] ) ? sanitize_key( (string) $d['id'] ) : '';
240 if ( '' === $d_id ) {
241 continue;
242 }
243 $d_label = isset( $d['label'] ) ? wp_strip_all_tags( (string) $d['label'] ) : '';
244 if ( '' === $d_label ) {
245 $d_label = $d_id;
246 }
247 // 64-char cap on labels — generous for any sensible
248 // human-typed desktop name, hard ceiling on meta size.
249 if ( strlen( $d_label ) > 64 ) {
250 $d_label = substr( $d_label, 0, 64 );
251 }
252 $clean_desktops[] = array(
253 'id' => $d_id,
254 'label' => $d_label,
255 );
256 $desktop_ids[] = $d_id;
257 if ( count( $clean_desktops ) >= OPENSTATION_SESSION_MAX_DESKTOPS ) {
258 break;
259 }
260 }
261 if ( ! empty( $clean_desktops ) ) {
262 $clean['desktops'] = $clean_desktops;
263 }
264 }
265 // Always at least one desktop in the persisted shape — guards
266 // against a client clearing every desktop and saving an empty
267 // list, or omitting the key entirely.
268 if ( empty( $clean['desktops'] ) ) {
269 $clean['desktops'] = array( openstation_default_desktop() );
270 }
271 if ( empty( $desktop_ids ) ) {
272 // Rebuild ids from the authoritative desktops list so the
273 // per-window desktopId validation below has something to
274 // compare against — otherwise a client that omits `desktops`
275 // but sends windows would hit `$desktop_ids[0]` on an empty
276 // array.
277 $desktop_ids = array_map(
278 static function ( $d ) {
279 return isset( $d['id'] ) ? (string) $d['id'] : '';
280 },
281 $clean['desktops']
282 );
283 $desktop_ids = array_values( array_filter( $desktop_ids ) );
284 if ( empty( $desktop_ids ) ) {
285 $desktop_ids = array( 'desktop-1' );
286 }
287 }
288
289 // --- Active desktop ------------------------------------------
290 if ( isset( $session['activeDesktop'] ) && is_string( $session['activeDesktop'] ) ) {
291 $candidate = sanitize_key( $session['activeDesktop'] );
292 if ( in_array( $candidate, $desktop_ids, true ) ) {
293 $clean['activeDesktop'] = $candidate;
294 }
295 }
296 // Fallback: first valid desktop. Already true via openstation_empty_session
297 // when the client passed nothing, but guards the case where
298 // activeDesktop named a desktop that didn't survive sanitization.
299 if ( ! in_array( $clean['activeDesktop'], $desktop_ids, true ) ) {
300 $clean['activeDesktop'] = $desktop_ids[0];
301 }
302
303 if ( isset( $session['windows'] ) && is_array( $session['windows'] ) ) {
304 foreach ( $session['windows'] as $win ) {
305 if ( ! is_array( $win ) ) {
306 continue;
307 }
308
309 $id = isset( $win['id'] ) ? sanitize_key( (string) $win['id'] ) : '';
310 if ( '' === $id ) {
311 continue;
312 }
313
314 // `baseId` groups multi-instance windows of the same admin page
315 // (e.g. `edit-php`, `edit-php-2`, `edit-php-3` all share baseId
316 // `edit-php`). Optional — older sessions predate the field and
317 // the client falls back to `id` when missing.
318 $base_id = isset( $win['baseId'] ) ? sanitize_key( (string) $win['baseId'] ) : '';
319 if ( '' === $base_id ) {
320 $base_id = $id;
321 }
322
323 // Native windows (OS Settings, Bug Report, anything from
324 // `openstation_register_window()`) carry no admin URL —
325 // the shell reconstructs them from the registry by id. Their
326 // `url` is a `#slug` marker, which would fail the same-admin
327 // check below and drop the window from the session entirely.
328 // Synthesise the marker server-side instead of trusting (or
329 // storing) whatever string the client sent: nothing ever
330 // navigates to it, so there is no reason to round-trip a
331 // client-controlled value through user meta.
332 $is_native = ! empty( $win['native'] );
333
334 if ( $is_native ) {
335 $url = '#' . $id;
336 } else {
337 $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 ) ) {
344 continue;
345 }
346 // Strip transient/routing flags before storage. The chromeless
347 // `openstation_chromeless` flag is an iframe-only concern and must never
348 // end up in a top-level URL (e.g., the portal's entry URL);
349 // the portal and classic flags only live on a single request.
350 $url = remove_query_arg(
351 array( 'openstation_chromeless', OPENSTATION_PORTAL_FLAG, OPENSTATION_CLASSIC_FLAG ),
352 $url
353 );
354 }
355
356 $state = isset( $win['state'] ) ? (string) $win['state'] : 'normal';
357 if ( ! in_array( $state, OPENSTATION_SESSION_STATES, true ) ) {
358 $state = 'normal';
359 }
360
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 $entry = array(
372 'id' => $id,
373 'baseId' => $base_id,
374 'desktopId' => $win_desktop,
375 'url' => $url,
376 'title' => isset( $win['title'] ) ? wp_strip_all_tags( (string) $win['title'] ) : '',
377 'icon' => isset( $win['icon'] ) ? sanitize_html_class( (string) $win['icon'] ) : 'dashicons-admin-generic',
378 'state' => $state,
379 'x' => openstation_sanitize_session_dimension( $win['x'] ?? 0, -10000, 10000 ),
380 'y' => openstation_sanitize_session_dimension( $win['y'] ?? 0, -10000, 10000 ),
381 'width' => openstation_sanitize_session_dimension( $win['width'] ?? 800, 0, 20000 ),
382 'height' => openstation_sanitize_session_dimension( $win['height'] ?? 600, 0, 20000 ),
383 );
384
385 // Marks the entry for the shell's restore path: native
386 // windows reopen through the native-window registry, not by
387 // pointing an iframe at a URL. Only written when true so
388 // sessions of plain admin windows keep their existing shape.
389 if ( $is_native ) {
390 $entry['native'] = true;
391
392 // A native window's open-time arguments: WHAT it is
393 // showing, as opposed to what it is. A native window
394 // is addressed by id, and its id is its identity
395 // (`desktop-mode-user-edit` is "the profile editor",
396 // not "the profile editor for user 12"), so a
397 // singleton that retargets has nowhere else to record
398 // its subject. Drop these and the window restores onto
399 // its default — the profile window comes back showing
400 // whoever is logged in, the customer window comes back
401 // empty.
402 //
403 // Only for native entries: an iframe window's URL
404 // already says what it shows, and it round-trips on
405 // its own.
406 $params = openstation_sanitize_session_params( $win['params'] ?? null );
407 if ( ! empty( $params ) ) {
408 $entry['params'] = $params;
409 }
410 }
411
412 // Sanitize external sub-tabs. Each entry carries a URL
413 // (any http/https — external tabs are explicitly for links
414 // OUT of wp-admin, so we don't restrict to same-origin
415 // here) and a label. Capped at a reasonable per-window
416 // limit so a runaway client can't balloon user meta.
417 if ( isset( $win['externalTabs'] ) && is_array( $win['externalTabs'] ) ) {
418 $tabs = array();
419 foreach ( $win['externalTabs'] as $tab ) {
420 if ( ! is_array( $tab ) ) {
421 continue;
422 }
423 $tab_url = isset( $tab['url'] ) ? esc_url_raw( (string) $tab['url'], array( 'http', 'https' ) ) : '';
424 if ( '' === $tab_url ) {
425 continue;
426 }
427 // Hard cap on URL length — a runaway client (or a
428 // malicious payload) could otherwise push many
429 // megabytes of URL into user meta. 2048 is the
430 // de-facto IE-legacy URL length limit and covers
431 // every real URL the shell restores.
432 if ( strlen( $tab_url ) > 2048 ) {
433 continue;
434 }
435 $label = isset( $tab['label'] ) ? wp_strip_all_tags( (string) $tab['label'] ) : '';
436 // Trim long labels server-side too, mirroring the
437 // client-side 80-char slice in the chromeless
438 // bridge. Keeps meta size predictable.
439 if ( strlen( $label ) > 80 ) {
440 $label = substr( $label, 0, 80 );
441 }
442 $tabs[] = array(
443 'url' => $tab_url,
444 'label' => $label,
445 );
446 if ( count( $tabs ) >= 16 ) {
447 break;
448 }
449 }
450 if ( ! empty( $tabs ) ) {
451 $entry['externalTabs'] = $tabs;
452 }
453 }
454
455 $clean['windows'][] = $entry;
456
457 if ( count( $clean['windows'] ) >= OPENSTATION_SESSION_MAX_WINDOWS ) {
458 break;
459 }
460 }
461 }
462
463 return $clean;
464 }
465
466 /**
467 * Clamps a numeric dimension into a sane range.
468 *
469 * Geometry coming from the client is untrusted. A malicious or buggy
470 * payload could try to stash multi-million-pixel values in meta,
471 * negative values that break the shell, or non-numeric garbage
472 * (strings, arrays, objects). This enforces numeric type and min/max
473 * bounds, falling back to `$min` for anything non-numeric so the
474 * window restores to a sane geometry rather than colliding with 0.
475 *
476 * Array/object input and non-numeric strings are rejected by
477 * `is_numeric()`; float `INF`/`NAN` pass that gate but cast to 0 and
478 * are then clamped into `[min, max]`, so no out-of-range value
479 * survives.
480 *
481 * @param mixed $value The raw value.
482 * @param int $min Minimum allowed value.
483 * @param int $max Maximum allowed value.
484 * @return int The clamped integer.
485 */
486 function openstation_sanitize_session_dimension( $value, $min, $max ) {
487 if ( is_string( $value ) ) {
488 $value = trim( $value );
489 }
490 if ( ! is_numeric( $value ) ) {
491 return (int) $min;
492 }
493 $value = (int) $value;
494 if ( $value < $min ) {
495 return (int) $min;
496 }
497 if ( $value > $max ) {
498 return (int) $max;
499 }
500 return $value;
501 }
502
503 /**
504 * Sanitize a native window's open-time params.
505 *
506 * These say WHAT a native window is showing (`{ userId: 12 }`,
507 * `{ customerId: 7 }`) as opposed to what it is — see
508 * `WindowConfig.params` on the JS side. They come from the client, so
509 * they are untrusted, unbounded, and arbitrarily nested unless this
510 * says otherwise.
511 *
512 * The rules mirror the client's own sanitizer so both ends agree on
513 * what survives: scalar values only (string, finite number, bool),
514 * and hard caps on both the number of keys and the length of a string
515 * value. Anything else is dropped rather than rejected — one careless
516 * value from a plugin must not cost the user every window's geometry.
517 *
518 * Keys are filtered to `[A-Za-z0-9_-]` rather than passed through
519 * `sanitize_key()`, which **lowercases**. Every param name in the
520 * shell is camelCase (`customerId`, `userId`), so lowercasing would
521 * store `customerid` and the client's `params.customerId` would read
522 * `undefined` — a window that restores blank, with the data sitting
523 * right there under a name nobody looks up.
524 *
525 * @param mixed $params Raw params from the payload.
526 * @return array Sanitized params, possibly empty.
527 */
528 function openstation_sanitize_session_params( $params ) {
529 if ( ! is_array( $params ) ) {
530 return array();
531 }
532
533 $clean = array();
534 foreach ( $params as $key => $value ) {
535 if ( count( $clean ) >= OPENSTATION_SESSION_MAX_PARAMS ) {
536 break;
537 }
538 $key = substr( preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $key ), 0, 64 );
539 if ( '' === $key ) {
540 continue;
541 }
542 if ( is_bool( $value ) ) {
543 $clean[ $key ] = $value;
544 continue;
545 }
546 if ( is_int( $value ) || is_float( $value ) ) {
547 if ( is_finite( (float) $value ) ) {
548 $clean[ $key ] = $value + 0;
549 }
550 continue;
551 }
552 if ( is_string( $value ) ) {
553 // A window param is an id, a slug or a short label. The
554 // cap keeps a runaway client from pushing megabytes into
555 // user meta, the same way the external-tab URL cap does.
556 $clean[ $key ] = substr( sanitize_text_field( $value ), 0, 256 );
557 }
558 }
559
560 return $clean;
561 }
562
563 /**
564 * Registers the REST routes used by the desktop shell to load and save
565 * the current user's session.
566 */
567 function openstation_register_session_rest_routes() {
568 register_rest_route(
569 'desktop-mode/v1',
570 '/session',
571 array(
572 array(
573 'methods' => WP_REST_Server::READABLE,
574 'callback' => 'openstation_rest_get_session',
575 'permission_callback' => 'openstation_rest_session_permission',
576 ),
577 array(
578 'methods' => WP_REST_Server::CREATABLE,
579 'callback' => 'openstation_rest_save_session',
580 'permission_callback' => 'openstation_rest_session_permission',
581 'args' => array(
582 'session' => array(
583 'required' => true,
584 'type' => 'object',
585 ),
586 ),
587 ),
588 array(
589 'methods' => WP_REST_Server::DELETABLE,
590 'callback' => 'openstation_rest_clear_session',
591 'permission_callback' => 'openstation_rest_session_permission',
592 ),
593 )
594 );
595 }
596 add_action( 'rest_api_init', 'openstation_register_session_rest_routes' );
597
598 /**
599 * Permission gate for the session REST routes: logged-in users who have
600 * OpenStation enabled. See {@see openstation_rest_require_enabled()}
601 * for why `read` alone is insufficient.
602 *
603 * @return true|WP_Error
604 */
605 function openstation_rest_session_permission() {
606 return openstation_rest_require_enabled();
607 }
608
609 /**
610 * GET /desktop-mode/v1/session — returns the caller's session.
611 *
612 * @return WP_REST_Response
613 */
614 function openstation_rest_get_session() {
615 return rest_ensure_response( openstation_get_session( get_current_user_id() ) );
616 }
617
618 /**
619 * POST /desktop-mode/v1/session — replaces the caller's session.
620 *
621 * @param WP_REST_Request $request The REST request.
622 * @return WP_REST_Response The stored session (after sanitization).
623 */
624 function openstation_rest_save_session( WP_REST_Request $request ) {
625 $user_id = get_current_user_id();
626 $payload = $request->get_param( 'session' );
627 openstation_save_session( $user_id, $payload );
628 return rest_ensure_response( openstation_get_session( $user_id ) );
629 }
630
631 /**
632 * DELETE /desktop-mode/v1/session — clears the caller's session.
633 *
634 * @return WP_REST_Response
635 */
636 function openstation_rest_clear_session() {
637 openstation_clear_session( get_current_user_id() );
638 return rest_ensure_response( openstation_empty_session() );
639 }
640