PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.9
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.9
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 0.8.6 All 33 releases
desktop-mode / includes / nonce-refresh.php

nonce-refresh.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.9, at includes/nonce-refresh.php

171 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Heartbeat-driven nonce refresh.
4 *
5 * WordPress nonces are valid for `nonce_life` (24 hours by default).
6 * The desktop shell is a long-running SPA whose per-window config
7 * blobs bake `wp_create_nonce()` values into the page at render
8 * time, so any session that stays open past the 24-hour mark hits
9 * `rest_cookie_invalid_nonce` ("Cookie check failed") on the next
10 * REST call — even though the auth cookie is still valid.
11 *
12 * Fix: on every Heartbeat tick, return a fresh copy of every nonce
13 * action the shell cares about, keyed by action string. The client
14 * subscribes via `src/nonce-refresh.ts` and rewrites the cached
15 * values in place. `wp_create_nonce()` returns the same value
16 * inside a single 12-hour tick window, so the actual nonce string
17 * only changes when the tick rolls — well before the 24-hour hard
18 * expiry catches the cached value.
19 *
20 * Default actions covered:
21 *
22 * - `wp_rest` — the canonical REST cookie nonce. Used by every
23 * window that stashes a `restNonce` in its config blob, plus
24 * the shell-wide auto-injection in `src/inject-rest-nonce.ts`.
25 * - `desktop-mode-plugins` — admin-ajax nonce for our
26 * browse/install/upload/reviews handlers.
27 * - `updates` — Core's wp.updates nonce used by
28 * `wp_ajax_install_plugin` / `wp_ajax_update_plugin`.
29 *
30 * Plugin authors who need to extend the set can hook
31 * `openstation_nonce_refresh_actions` and add their own nonce
32 * action strings. The client side picks the new fields up
33 * automatically through the same heartbeat field — feature modules
34 * just need to register a target for the field they care about via
35 * the JS-side `registerNonceTarget()` helper.
36 *
37 * @package OpenStation
38 */
39
40 defined( 'ABSPATH' ) || exit;
41
42 /**
43 * Heartbeat field name. Public — `src/nonce-refresh.ts` subscribes
44 * to this string. Keep the value stable across versions or update
45 * both ends.
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_NONCE_REFRESH_FIELD = 'desktop_mode_nonces';
54
55 /**
56 * Heartbeat field carrying the authenticated user's identity.
57 * `src/auth-recovery/index.ts` compares `uid` against the shell's
58 * boot-time viewer and hard-reloads when a *different* user logged
59 * in through the session-expired prompt — in-place nonce refresh
60 * would otherwise leave user A's desktop issuing user B's requests.
61 *
62 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
63 * persisted or externally-visible identifier, so renaming it would
64 * orphan data already written by live installs (or break a live
65 * URL). The mismatch between this constant's name and its value is
66 * deliberate — it is NOT a half-finished rename.
67 */
68 const OPENSTATION_AUTH_FIELD = 'desktop_mode_auth';
69
70 /**
71 * Mint a fresh map of `{ action => nonce }` for every action the
72 * shell needs to keep alive past `nonce_life`. The set is
73 * filterable so other native windows / third-party plugins can
74 * extend it; the only requirement is that the action string match
75 * whatever was passed to `wp_create_nonce()` at registration.
76 *
77 * @return array<string,string> Map of nonce-action => current nonce value.
78 */
79 function openstation_nonce_refresh_build_payload() {
80 $actions = array(
81 'wp_rest',
82 'desktop-mode-plugins',
83 'updates',
84 );
85
86 /**
87 * Filter the set of nonce actions refreshed on every Heartbeat tick.
88 *
89 * Each entry must be a literal nonce action string (the same value
90 * passed to `wp_create_nonce()` wherever the original was minted).
91 *
92 * @param string[] $actions Default nonce actions.
93 */
94 $actions = (array) apply_filters( 'openstation_nonce_refresh_actions', $actions );
95
96 $payload = array();
97 foreach ( $actions as $action ) {
98 if ( ! is_string( $action ) || '' === $action ) {
99 continue;
100 }
101 $payload[ $action ] = wp_create_nonce( $action );
102 }
103 return $payload;
104 }
105
106 /**
107 * Heartbeat handler — attach the fresh nonce map to every tick
108 * from a user who has OpenStation enabled.
109 *
110 * Gated on `openstation_is_enabled()` (not just `is_user_logged_in()`)
111 * so users on classic admin screens — editors on post-edit pages,
112 * subscribers reading the front-end heartbeat — don't carry the
113 * payload around. The shell's nonces only need refreshing for
114 * users who actually run the shell.
115 *
116 * The cost is tiny when fired (three `wp_create_nonce()` calls,
117 * all hot-cached inside a single request) — the gate is about
118 * not shipping irrelevant data to non-shell users on every tick.
119 *
120 * @param array $response Heartbeat response (filter return value).
121 * @param array $data Client-sent payload. Unused here.
122 * @return array
123 */
124 function openstation_nonce_refresh_heartbeat_received( $response, $data ) {
125 unset( $data );
126 if ( ! is_array( $response ) ) {
127 $response = array();
128 }
129 if ( ! function_exists( 'openstation_is_enabled' ) || ! openstation_is_enabled() ) {
130 return $response;
131 }
132 $response[ OPENSTATION_NONCE_REFRESH_FIELD ] = openstation_nonce_refresh_build_payload();
133 $response[ OPENSTATION_AUTH_FIELD ] = array( 'uid' => get_current_user_id() );
134 return $response;
135 }
136 add_filter( 'heartbeat_received', 'openstation_nonce_refresh_heartbeat_received', 5, 2 );
137
138 /**
139 * Nonce-refresh rider for the `nonces_expired` heartbeat path.
140 *
141 * When the Heartbeat POST arrives with a stale `heartbeat-nonce`
142 * (the first tick after a re-login, or any tick once the nonce
143 * aged past `nonce_life`), core short-circuits before
144 * `heartbeat_received` / `heartbeat_send` ever run — the response
145 * is built solely from the `wp_refresh_nonces` filter. Without
146 * this hook the shell would only receive fresh
147 * `desktop_mode_nonces` on the FOLLOWING tick, leaving a window
148 * where every cached nonce is rejected ("Cookie check failed").
149 *
150 * Riding the same payload here means one round-trip heals the
151 * shell: the tick that says "your nonces expired" also delivers
152 * the replacements. Client-side, `heartbeat.js` still fires
153 * `heartbeat-tick` for this response, so the regular
154 * `src/nonce-refresh.ts` subscriber picks the map up unchanged.
155 *
156 * @param array $response Heartbeat response (filter return value).
157 * @return array
158 */
159 function openstation_nonce_refresh_on_expired( $response ) {
160 if ( ! is_array( $response ) ) {
161 $response = array();
162 }
163 if ( ! function_exists( 'openstation_is_enabled' ) || ! openstation_is_enabled() ) {
164 return $response;
165 }
166 $response[ OPENSTATION_NONCE_REFRESH_FIELD ] = openstation_nonce_refresh_build_payload();
167 $response[ OPENSTATION_AUTH_FIELD ] = array( 'uid' => get_current_user_id() );
168 return $response;
169 }
170 add_filter( 'wp_refresh_nonces', 'openstation_nonce_refresh_on_expired', 5 );
171