nonce }` for every action the * shell needs to keep alive past `nonce_life`. The set is * filterable so other native windows / third-party plugins can * extend it; the only requirement is that the action string match * whatever was passed to `wp_create_nonce()` at registration. * * @return array Map of nonce-action => current nonce value. */ function desktop_mode_nonce_refresh_build_payload() { $actions = array( 'wp_rest', 'desktop-mode-plugins', 'updates', ); /** * Filter the set of nonce actions refreshed on every Heartbeat tick. * * Each entry must be a literal nonce action string (the same value * passed to `wp_create_nonce()` wherever the original was minted). * * @param string[] $actions Default nonce actions. */ $actions = (array) apply_filters( 'desktop_mode_nonce_refresh_actions', $actions ); $payload = array(); foreach ( $actions as $action ) { if ( ! is_string( $action ) || $action === '' ) { continue; } $payload[ $action ] = wp_create_nonce( $action ); } return $payload; } /** * Heartbeat handler — attach the fresh nonce map to every tick * from a user who has Desktop Mode enabled. * * Gated on `desktop_mode_is_enabled()` (not just `is_user_logged_in()`) * so users on classic admin screens — editors on post-edit pages, * subscribers reading the front-end heartbeat — don't carry the * payload around. The shell's nonces only need refreshing for * users who actually run the shell. * * The cost is tiny when fired (three `wp_create_nonce()` calls, * all hot-cached inside a single request) — the gate is about * not shipping irrelevant data to non-shell users on every tick. * * @param array $response Heartbeat response (filter return value). * @param array $data Client-sent payload. Unused here. * @return array */ function desktop_mode_nonce_refresh_heartbeat_received( $response, $data ) { unset( $data ); if ( ! is_array( $response ) ) { $response = array(); } if ( ! function_exists( 'desktop_mode_is_enabled' ) || ! desktop_mode_is_enabled() ) { return $response; } $response[ DESKTOP_MODE_NONCE_REFRESH_FIELD ] = desktop_mode_nonce_refresh_build_payload(); $response[ DESKTOP_MODE_AUTH_FIELD ] = array( 'uid' => get_current_user_id() ); return $response; } add_filter( 'heartbeat_received', 'desktop_mode_nonce_refresh_heartbeat_received', 5, 2 ); /** * Nonce-refresh rider for the `nonces_expired` heartbeat path. * * When the Heartbeat POST arrives with a stale `heartbeat-nonce` * (the first tick after a re-login, or any tick once the nonce * aged past `nonce_life`), core short-circuits before * `heartbeat_received` / `heartbeat_send` ever run — the response * is built solely from the `wp_refresh_nonces` filter. Without * this hook the shell would only receive fresh * `desktop_mode_nonces` on the FOLLOWING tick, leaving a window * where every cached nonce is rejected ("Cookie check failed"). * * Riding the same payload here means one round-trip heals the * shell: the tick that says "your nonces expired" also delivers * the replacements. Client-side, `heartbeat.js` still fires * `heartbeat-tick` for this response, so the regular * `src/nonce-refresh.ts` subscriber picks the map up unchanged. * * @param array $response Heartbeat response (filter return value). * @return array */ function desktop_mode_nonce_refresh_on_expired( $response ) { if ( ! is_array( $response ) ) { $response = array(); } if ( ! function_exists( 'desktop_mode_is_enabled' ) || ! desktop_mode_is_enabled() ) { return $response; } $response[ DESKTOP_MODE_NONCE_REFRESH_FIELD ] = desktop_mode_nonce_refresh_build_payload(); $response[ DESKTOP_MODE_AUTH_FIELD ] = array( 'uid' => get_current_user_id() ); return $response; } add_filter( 'wp_refresh_nonces', 'desktop_mode_nonce_refresh_on_expired', 5 );