PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.8
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.8, at includes/nonce-refresh.php

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