PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.7
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 / nonce-refresh.php

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

170 lines 6.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — 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 * `desktop_mode_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 WPDesktopMode
38 * @since 0.8.7
39 */
40
41 defined( 'ABSPATH' ) || exit;
42
43 /**
44 * Heartbeat field name. Public — `src/nonce-refresh.ts` subscribes
45 * to this string. Keep the value stable across versions or update
46 * both ends.
47 */
48 const DESKTOP_MODE_NONCE_REFRESH_FIELD = 'desktop_mode_nonces';
49
50 /**
51 * Heartbeat field carrying the authenticated user's identity.
52 * `src/auth-recovery/index.ts` compares `uid` against the shell's
53 * boot-time viewer and hard-reloads when a *different* user logged
54 * in through the session-expired prompt — in-place nonce refresh
55 * would otherwise leave user A's desktop issuing user B's requests.
56 *
57 * @since 0.9.8
58 */
59 const DESKTOP_MODE_AUTH_FIELD = 'desktop_mode_auth';
60
61 /**
62 * Mint a fresh map of `{ action => nonce }` for every action the
63 * shell needs to keep alive past `nonce_life`. The set is
64 * filterable so other native windows / third-party plugins can
65 * extend it; the only requirement is that the action string match
66 * whatever was passed to `wp_create_nonce()` at registration.
67 *
68 * @since 0.8.7
69 *
70 * @return array<string,string> Map of nonce-action => current nonce value.
71 */
72 function desktop_mode_nonce_refresh_build_payload() {
73 $actions = array(
74 'wp_rest',
75 'desktop-mode-plugins',
76 'updates',
77 );
78
79 /**
80 * Filter the set of nonce actions refreshed on every Heartbeat tick.
81 *
82 * Each entry must be a literal nonce action string (the same value
83 * passed to `wp_create_nonce()` wherever the original was minted).
84 *
85 * @since 0.8.7
86 *
87 * @param string[] $actions Default nonce actions.
88 */
89 $actions = (array) apply_filters( 'desktop_mode_nonce_refresh_actions', $actions );
90
91 $payload = array();
92 foreach ( $actions as $action ) {
93 if ( ! is_string( $action ) || $action === '' ) {
94 continue;
95 }
96 $payload[ $action ] = wp_create_nonce( $action );
97 }
98 return $payload;
99 }
100
101 /**
102 * Heartbeat handler — attach the fresh nonce map to every tick
103 * from a user who has Desktop Mode enabled.
104 *
105 * Gated on `desktop_mode_is_enabled()` (not just `is_user_logged_in()`)
106 * so users on classic admin screens — editors on post-edit pages,
107 * subscribers reading the front-end heartbeat — don't carry the
108 * payload around. The shell's nonces only need refreshing for
109 * users who actually run the shell.
110 *
111 * The cost is tiny when fired (three `wp_create_nonce()` calls,
112 * all hot-cached inside a single request) — the gate is about
113 * not shipping irrelevant data to non-shell users on every tick.
114 *
115 * @since 0.8.7
116 *
117 * @param array $response Heartbeat response (filter return value).
118 * @param array $data Client-sent payload. Unused here.
119 * @return array
120 */
121 function desktop_mode_nonce_refresh_heartbeat_received( $response, $data ) {
122 unset( $data );
123 if ( ! is_array( $response ) ) {
124 $response = array();
125 }
126 if ( ! function_exists( 'desktop_mode_is_enabled' ) || ! desktop_mode_is_enabled() ) {
127 return $response;
128 }
129 $response[ DESKTOP_MODE_NONCE_REFRESH_FIELD ] = desktop_mode_nonce_refresh_build_payload();
130 $response[ DESKTOP_MODE_AUTH_FIELD ] = array( 'uid' => get_current_user_id() );
131 return $response;
132 }
133 add_filter( 'heartbeat_received', 'desktop_mode_nonce_refresh_heartbeat_received', 5, 2 );
134
135 /**
136 * Nonce-refresh rider for the `nonces_expired` heartbeat path.
137 *
138 * When the Heartbeat POST arrives with a stale `heartbeat-nonce`
139 * (the first tick after a re-login, or any tick once the nonce
140 * aged past `nonce_life`), core short-circuits before
141 * `heartbeat_received` / `heartbeat_send` ever run — the response
142 * is built solely from the `wp_refresh_nonces` filter. Without
143 * this hook the shell would only receive fresh
144 * `desktop_mode_nonces` on the FOLLOWING tick, leaving a window
145 * where every cached nonce is rejected ("Cookie check failed").
146 *
147 * Riding the same payload here means one round-trip heals the
148 * shell: the tick that says "your nonces expired" also delivers
149 * the replacements. Client-side, `heartbeat.js` still fires
150 * `heartbeat-tick` for this response, so the regular
151 * `src/nonce-refresh.ts` subscriber picks the map up unchanged.
152 *
153 * @since 0.9.8
154 *
155 * @param array $response Heartbeat response (filter return value).
156 * @return array
157 */
158 function desktop_mode_nonce_refresh_on_expired( $response ) {
159 if ( ! is_array( $response ) ) {
160 $response = array();
161 }
162 if ( ! function_exists( 'desktop_mode_is_enabled' ) || ! desktop_mode_is_enabled() ) {
163 return $response;
164 }
165 $response[ DESKTOP_MODE_NONCE_REFRESH_FIELD ] = desktop_mode_nonce_refresh_build_payload();
166 $response[ DESKTOP_MODE_AUTH_FIELD ] = array( 'uid' => get_current_user_id() );
167 return $response;
168 }
169 add_filter( 'wp_refresh_nonces', 'desktop_mode_nonce_refresh_on_expired', 5 );
170