PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
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
← All changes | includes/oauth-relay.php +138 -132 0.8.91.1.10 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — OAuth relay scaffolding.
3 + * OpenStation — OAuth relay scaffolding.
4 4 *
5 5 * Every plugin that integrates with an external service (Tumblr,
6 6 * Mastodon, Bluesky, Spotify, Discord, …) reinvents the same
7 7 * fiddly OAuth dance: generate a `state` nonce, persist it in a
@@ -17,9 +17,9 @@
17 17 * here and is identical across consumers.
18 18 *
19 19 * Public PHP surface:
20 20 *
21 - * desktop_mode_register_oauth_relay( $service, [
21 + * openstation_register_oauth_relay( $service, [
22 22 * 'authorize_url' => 'https://www.example.com/oauth2/authorize',
23 23 * 'token_url' => 'https://api.example.com/oauth2/token',
24 24 * 'client_id' => 'CLIENT_ID',
25 25 * 'client_secret' => 'CLIENT_SECRET',
@@ -28,33 +28,38 @@
28 28 * // Persist tokens however your plugin needs.
29 29 * },
30 30 * ] );
31 31 *
32 - * Public JS surface (since 0.8.2):
32 + * Public JS surface:
33 33 *
34 - * const { tokens } = await wp.desktop.startOAuth( 'example' );
34 + * const { ok, service } = await wp.os.startOAuth( 'example' );
35 + * // Tokens stay server-side — persisted by your `on_success` callback.
35 36 *
36 37 * REST routes:
37 38 *
38 39 * POST /desktop-mode/v1/oauth/start body: { service: string }
39 40 * → { authorize_url: string, state: string }
40 - * GET /desktop-mode/v1/oauth/callback ?service&code&state
41 + * GET /desktop-mode/v1/oauth/callback ?code&state&error
41 42 * → HTML page that postMessages the opener and closes
42 43 *
43 - * @package Desktop_Mode
44 - * @since 0.8.2
44 + * @package OpenStation
45 45 */
46 46
47 47 defined( 'ABSPATH' ) || exit;
48 48
49 -const DESKTOP_MODE_OAUTH_TRANSIENT_PREFIX = 'desktop_mode_oauth_state_';
50 -const DESKTOP_MODE_OAUTH_STATE_TTL = 600; // 10 minutes.
49 +/**
50 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
51 + * persisted or externally-visible identifier, so renaming it would
52 + * orphan data already written by live installs (or break a live
53 + * URL). The mismatch between this constant's name and its value is
54 + * deliberate — it is NOT a half-finished rename.
55 + */
56 +const OPENSTATION_OAUTH_TRANSIENT_PREFIX = 'desktop_mode_oauth_state_';
57 +const OPENSTATION_OAUTH_STATE_TTL = 600; // 10 minutes.
51 58
52 59 /**
53 60 * Register an OAuth relay for `$service`.
54 61 *
55 - * @since 0.8.2
56 - *
57 62 * @param string $service Slug identifying the service. Lowercased,
58 63 * sanitized via `sanitize_key`.
59 64 * @param array $args {
60 65 * OAuth relay configuration.
@@ -84,13 +89,13 @@
84 89 * logged-in user).
85 90 * }
86 91 * @return true|WP_Error `true` on success, `WP_Error` on validation failure.
87 92 */
88 -function desktop_mode_register_oauth_relay( $service, $args = array() ) {
93 +function openstation_register_oauth_relay( $service, $args = array() ) {
89 94 $service = sanitize_key( (string) $service );
90 95 if ( '' === $service ) {
91 96 return new WP_Error(
92 - 'desktop_mode_oauth_missing_service',
97 + 'openstation_oauth_missing_service',
93 98 __( 'OAuth relay registration requires a non-empty service slug.', 'desktop-mode' )
94 99 );
95 100 }
96 101
@@ -102,14 +107,14 @@
102 107 'scope' => '',
103 108 'on_success' => null,
104 109 'capabilities' => array( 'read' ),
105 110 );
106 - $args = wp_parse_args( $args, $defaults );
111 + $args = wp_parse_args( $args, $defaults );
107 112
108 113 foreach ( array( 'authorize_url', 'token_url', 'client_id', 'client_secret' ) as $required ) {
109 114 if ( '' === (string) $args[ $required ] ) {
110 115 return new WP_Error(
111 - 'desktop_mode_oauth_missing_' . $required,
116 + 'openstation_oauth_missing_' . $required,
112 117 /* translators: %s: missing field name. */
113 118 sprintf( __( 'OAuth relay registration requires a non-empty `%s`.', 'desktop-mode' ), $required ),
114 119 array( 'service' => $service )
115 120 );
@@ -117,9 +122,9 @@
117 122 }
118 123
119 124 if ( ! is_callable( $args['on_success'] ) ) {
120 125 return new WP_Error(
121 - 'desktop_mode_oauth_missing_on_success',
126 + 'openstation_oauth_missing_on_success',
122 127 __( 'OAuth relay registration requires a callable `on_success` handler.', 'desktop-mode' ),
123 128 array( 'service' => $service )
124 129 );
125 130 }
@@ -127,9 +132,9 @@
127 132 $authorize_url = esc_url_raw( (string) $args['authorize_url'], array( 'http', 'https' ) );
128 133 $token_url = esc_url_raw( (string) $args['token_url'], array( 'http', 'https' ) );
129 134 if ( '' === $authorize_url || '' === $token_url ) {
130 135 return new WP_Error(
131 - 'desktop_mode_oauth_invalid_url',
136 + 'openstation_oauth_invalid_url',
132 137 __( 'OAuth relay `authorize_url` and `token_url` must be valid http(s) URLs.', 'desktop-mode' ),
133 138 array( 'service' => $service )
134 139 );
135 140 }
@@ -143,22 +148,20 @@
143 148 'scope' => (string) $args['scope'],
144 149 'on_success' => $args['on_success'],
145 150 'capabilities' => array_values( array_filter( array_map( 'strval', (array) $args['capabilities'] ) ) ),
146 151 );
147 - desktop_mode_oauth_relay_registry( $service, $entry );
152 + openstation_oauth_relay_registry( $service, $entry );
148 153
149 154 /**
150 155 * Fires after an OAuth relay is registered. Use this to layer
151 156 * observability or to extend behaviour.
152 157 *
153 - * @since 0.8.2
154 - *
155 158 * @param string $service The service slug.
156 159 * @param array $entry The stored registry entry minus the secrets
157 160 * (`client_secret` is masked).
158 161 */
159 162 do_action(
160 - 'desktop_mode_oauth_relay_registered',
163 + 'openstation_oauth_relay_registered',
161 164 $service,
162 165 array_merge( $entry, array( 'client_secret' => '[redacted]' ) )
163 166 );
164 167
@@ -168,12 +171,11 @@
168 171 /**
169 172 * Static registry for OAuth relays. Mirror of the icon / native-
170 173 * window / wallpaper registries.
171 174 *
172 - * @since 0.8.2
173 175 * @internal
174 176 */
175 -function desktop_mode_oauth_relay_registry( $service = '', $entry = null ) {
177 +function openstation_oauth_relay_registry( $service = '', $entry = null ) {
176 178 static $store = array();
177 179
178 180 if ( '' === (string) $service ) {
179 181 return $store;
@@ -189,58 +191,54 @@
189 191 }
190 192
191 193 /**
192 194 * Remove a previously registered OAuth relay. Mirror of
193 - * `desktop_mode_register_oauth_relay()` — handy for plugins that
195 + * `openstation_register_oauth_relay()` — handy for plugins that
194 196 * register conditionally and for PHPUnit teardowns.
195 197 *
196 - * @since 0.8.2
197 - *
198 198 * @param string $service Service slug passed to register.
199 199 * @return void
200 200 */
201 -function desktop_mode_unregister_oauth_relay( $service ) {
201 +function openstation_unregister_oauth_relay( $service ) {
202 202 $service = sanitize_key( (string) $service );
203 203 if ( '' === $service ) {
204 204 return;
205 205 }
206 - desktop_mode_oauth_relay_registry( $service, '__unset__' );
206 + openstation_oauth_relay_registry( $service, '__unset__' );
207 207 }
208 208
209 209 /**
210 210 * The redirect URI the popup posts back to. Same for every service —
211 - * `service` is carried as a query arg.
211 + * the framework recovers the service from the state transient, so no
212 + * service query arg is needed.
212 213 *
213 - * @since 0.8.2
214 - *
215 214 * @return string
216 215 */
217 -function desktop_mode_oauth_redirect_uri() {
216 +function openstation_oauth_redirect_uri() {
218 217 return rest_url( 'desktop-mode/v1/oauth/callback' );
219 218 }
220 219
221 220 /**
222 221 * Generate a fresh state nonce, persist it in a transient keyed by
223 - * `user_id + state`, and return the value the popup will round-trip.
222 + * the state value (with `user_id` + `service` stored in the transient
223 + * payload), and return the value the popup will round-trip.
224 224 *
225 - * @since 0.8.2
226 - *
227 225 * @param int $user_id The user starting the flow.
228 226 * @param string $service The service slug being authorized.
229 227 * @return string The state value to embed in the authorize URL.
230 228 */
231 -function desktop_mode_oauth_issue_state( $user_id, $service ) {
229 +function openstation_oauth_issue_state( $user_id, $service ) {
232 230 // 32 chars of letters+digits — `wp_generate_password` with the
233 231 // no-special-chars flag is the canonical WP shape.
234 232 $state = wp_generate_password( 32, false );
235 233 set_transient(
236 - DESKTOP_MODE_OAUTH_TRANSIENT_PREFIX . $state,
234 + OPENSTATION_OAUTH_TRANSIENT_PREFIX . $state,
237 235 array(
238 236 'user_id' => (int) $user_id,
239 237 'service' => (string) $service,
240 238 'issued' => time(),
241 239 ),
242 - DESKTOP_MODE_OAUTH_STATE_TTL
240 + OPENSTATION_OAUTH_STATE_TTL
243 241 );
244 242 return $state;
245 243 }
246 244
@@ -250,19 +248,17 @@
250 248 *
251 249 * Single-use: a successful read deletes the transient so a replay
252 250 * with the same state fails.
253 251 *
254 - * @since 0.8.2
255 - *
256 252 * @param string $state State value from the callback query.
257 253 * @return array{user_id:int,service:string,issued:int}|null
258 254 */
259 -function desktop_mode_oauth_consume_state( $state ) {
255 +function openstation_oauth_consume_state( $state ) {
260 256 $state = (string) $state;
261 257 if ( '' === $state ) {
262 258 return null;
263 259 }
264 - $key = DESKTOP_MODE_OAUTH_TRANSIENT_PREFIX . $state;
260 + $key = OPENSTATION_OAUTH_TRANSIENT_PREFIX . $state;
265 261 $entry = get_transient( $key );
266 262 if ( ! is_array( $entry ) || empty( $entry['user_id'] ) || empty( $entry['service'] ) ) {
267 263 return null;
268 264 }
@@ -277,19 +273,17 @@
277 273 /**
278 274 * REST: `POST /desktop-mode/v1/oauth/start` — issue a state and
279 275 * return the assembled authorize URL.
280 276 *
281 - * @since 0.8.2
282 - *
283 277 * @param WP_REST_Request $request
284 278 * @return WP_REST_Response|WP_Error
285 279 */
286 -function desktop_mode_rest_oauth_start( WP_REST_Request $request ) {
280 +function openstation_rest_oauth_start( WP_REST_Request $request ) {
287 281 $service = sanitize_key( (string) $request->get_param( 'service' ) );
288 - $entry = desktop_mode_oauth_relay_registry( $service );
282 + $entry = openstation_oauth_relay_registry( $service );
289 283 if ( ! is_array( $entry ) ) {
290 284 return new WP_Error(
291 - 'desktop_mode_oauth_unknown_service',
285 + 'openstation_oauth_unknown_service',
292 286 __( 'No OAuth relay is registered for that service.', 'desktop-mode' ),
293 287 array( 'status' => 404 )
294 288 );
295 289 }
@@ -296,9 +290,9 @@
296 290
297 291 foreach ( $entry['capabilities'] as $cap ) {
298 292 if ( ! current_user_can( (string) $cap ) ) {
299 293 return new WP_Error(
300 - 'desktop_mode_oauth_capability_denied',
294 + 'openstation_oauth_capability_denied',
301 295 __( 'Current user lacks the capability required to start this OAuth flow.', 'desktop-mode' ),
302 296 array( 'status' => 403 )
303 297 );
304 298 }
@@ -304,14 +298,14 @@
304 298 }
305 299 }
306 300
307 301 $user_id = get_current_user_id();
308 - $state = desktop_mode_oauth_issue_state( $user_id, $service );
302 + $state = openstation_oauth_issue_state( $user_id, $service );
309 303
310 304 $query = array(
311 305 'response_type' => 'code',
312 306 'client_id' => $entry['client_id'],
313 - 'redirect_uri' => desktop_mode_oauth_redirect_uri(),
307 + 'redirect_uri' => openstation_oauth_redirect_uri(),
314 308 'state' => $state,
315 309 );
316 310 if ( '' !== $entry['scope'] ) {
317 311 $query['scope'] = $entry['scope'];
@@ -322,16 +316,14 @@
322 316 * Lets plugins inject service-specific extras (`access_type=offline`
323 317 * for Google, `force_login=true` for Twitter, `prompt=consent`,
324 318 * etc.) without having to fork the relay.
325 319 *
326 - * @since 0.8.2
327 - *
328 320 * @param array $query Default query params.
329 321 * @param string $service Service slug.
330 322 * @param array $entry Registry entry (with secrets redacted).
331 323 */
332 324 $query = apply_filters(
333 - 'desktop_mode_oauth_authorize_query',
325 + 'openstation_oauth_authorize_query',
334 326 $query,
335 327 $service,
336 328 array_merge( $entry, array( 'client_secret' => '[redacted]' ) )
337 329 );
@@ -350,54 +342,60 @@
350 342 * REST: `GET /desktop-mode/v1/oauth/callback` — exchange the auth
351 343 * code for tokens, fire the registered `on_success` handler, then
352 344 * render an HTML page that `postMessage`s the opener and closes.
353 345 *
354 - * @since 0.8.2
355 - *
356 346 * @param WP_REST_Request $request
357 347 * @return WP_REST_Response|WP_Error
358 348 */
359 -function desktop_mode_rest_oauth_callback( WP_REST_Request $request ) {
349 +function openstation_rest_oauth_callback( WP_REST_Request $request ) {
360 350 $state = (string) $request->get_param( 'state' );
361 351 $code = (string) $request->get_param( 'code' );
362 352 $error = (string) $request->get_param( 'error' );
363 353
364 - $consumed = desktop_mode_oauth_consume_state( $state );
354 + $consumed = openstation_oauth_consume_state( $state );
365 355 if ( null === $consumed ) {
366 - return desktop_mode_oauth_render_callback_html( array(
367 - 'ok' => false,
368 - 'reason' => 'invalid_state',
369 - 'message' => __( 'OAuth state nonce missing, expired, or already used.', 'desktop-mode' ),
370 - ) );
356 + return openstation_oauth_render_callback_html(
357 + array(
358 + 'ok' => false,
359 + 'reason' => 'invalid_state',
360 + 'message' => __( 'OAuth state nonce missing, expired, or already used.', 'desktop-mode' ),
361 + )
362 + );
371 363 }
372 364 $service = $consumed['service'];
373 365 $user_id = $consumed['user_id'];
374 366
375 367 if ( '' !== $error ) {
376 - return desktop_mode_oauth_render_callback_html( array(
377 - 'ok' => false,
378 - 'service' => $service,
379 - 'reason' => 'authorize_denied',
380 - 'message' => $error,
381 - ) );
368 + return openstation_oauth_render_callback_html(
369 + array(
370 + 'ok' => false,
371 + 'service' => $service,
372 + 'reason' => 'authorize_denied',
373 + 'message' => $error,
374 + )
375 + );
382 376 }
383 377
384 - $entry = desktop_mode_oauth_relay_registry( $service );
378 + $entry = openstation_oauth_relay_registry( $service );
385 379 if ( ! is_array( $entry ) ) {
386 - return desktop_mode_oauth_render_callback_html( array(
387 - 'ok' => false,
388 - 'reason' => 'unknown_service',
389 - 'message' => __( 'OAuth relay is no longer registered for that service.', 'desktop-mode' ),
390 - ) );
380 + return openstation_oauth_render_callback_html(
381 + array(
382 + 'ok' => false,
383 + 'reason' => 'unknown_service',
384 + 'message' => __( 'OAuth relay is no longer registered for that service.', 'desktop-mode' ),
385 + )
386 + );
391 387 }
392 388
393 389 if ( '' === $code ) {
394 - return desktop_mode_oauth_render_callback_html( array(
395 - 'ok' => false,
396 - 'service' => $service,
397 - 'reason' => 'missing_code',
398 - 'message' => __( 'OAuth callback did not return an authorization code.', 'desktop-mode' ),
399 - ) );
390 + return openstation_oauth_render_callback_html(
391 + array(
392 + 'ok' => false,
393 + 'service' => $service,
394 + 'reason' => 'missing_code',
395 + 'message' => __( 'OAuth callback did not return an authorization code.', 'desktop-mode' ),
396 + )
397 + );
400 398 }
401 399
402 400 $response = wp_remote_post(
403 401 $entry['token_url'],
@@ -407,46 +405,52 @@
407 405 'grant_type' => 'authorization_code',
408 406 'code' => $code,
409 407 'client_id' => $entry['client_id'],
410 408 'client_secret' => $entry['client_secret'],
411 - 'redirect_uri' => desktop_mode_oauth_redirect_uri(),
409 + 'redirect_uri' => openstation_oauth_redirect_uri(),
412 410 ),
413 411 'headers' => array( 'Accept' => 'application/json' ),
414 412 )
415 413 );
416 414 if ( is_wp_error( $response ) ) {
417 - return desktop_mode_oauth_render_callback_html( array(
418 - 'ok' => false,
419 - 'service' => $service,
420 - 'reason' => 'token_request_failed',
421 - 'message' => $response->get_error_message(),
422 - ) );
415 + return openstation_oauth_render_callback_html(
416 + array(
417 + 'ok' => false,
418 + 'service' => $service,
419 + 'reason' => 'token_request_failed',
420 + 'message' => $response->get_error_message(),
421 + )
422 + );
423 423 }
424 424 $status = (int) wp_remote_retrieve_response_code( $response );
425 425 $body = wp_remote_retrieve_body( $response );
426 426 $tokens = json_decode( $body, true );
427 427 if ( $status < 200 || $status >= 300 || ! is_array( $tokens ) ) {
428 - return desktop_mode_oauth_render_callback_html( array(
429 - 'ok' => false,
430 - 'service' => $service,
431 - 'reason' => 'token_exchange_failed',
432 - 'message' => sprintf(
428 + return openstation_oauth_render_callback_html(
429 + array(
430 + 'ok' => false,
431 + 'service' => $service,
432 + 'reason' => 'token_exchange_failed',
433 + 'message' => sprintf(
433 434 /* translators: %d: HTTP status code. */
434 - __( 'Token exchange failed with HTTP %d.', 'desktop-mode' ),
435 - $status
436 - ),
437 - ) );
435 + __( 'Token exchange failed with HTTP %d.', 'desktop-mode' ),
436 + $status
437 + ),
438 + )
439 + );
438 440 }
439 441
440 442 try {
441 443 call_user_func( $entry['on_success'], $user_id, $tokens, $service );
442 444 } catch ( \Throwable $e ) {
443 - return desktop_mode_oauth_render_callback_html( array(
444 - 'ok' => false,
445 - 'service' => $service,
446 - 'reason' => 'on_success_threw',
447 - 'message' => $e->getMessage(),
448 - ) );
445 + return openstation_oauth_render_callback_html(
446 + array(
447 + 'ok' => false,
448 + 'service' => $service,
449 + 'reason' => 'on_success_threw',
450 + 'message' => $e->getMessage(),
451 + )
452 + );
449 453 }
450 454
451 455 /**
452 456 * Fires after a successful OAuth round-trip — after `on_success`
@@ -453,19 +457,19 @@
453 457 * persists the tokens. Plugins use this to refresh badges,
454 458 * re-render dock items, or surface a "connected" toast in
455 459 * sibling windows via the activity bus.
456 460 *
457 - * @since 0.8.2
458 - *
459 461 * @param string $service Service slug.
460 462 * @param int $user_id User who connected.
461 463 */
462 - do_action( 'desktop_mode_oauth_relay_connected', $service, $user_id );
464 + do_action( 'openstation_oauth_relay_connected', $service, $user_id );
463 465
464 - return desktop_mode_oauth_render_callback_html( array(
465 - 'ok' => true,
466 - 'service' => $service,
467 - ) );
466 + return openstation_oauth_render_callback_html(
467 + array(
468 + 'ok' => true,
469 + 'service' => $service,
470 + )
471 + );
468 472 }
469 473
470 474 /**
471 475 * Build the HTML string the OAuth callback popup renders.
@@ -470,9 +474,9 @@
470 474 /**
471 475 * Build the HTML string the OAuth callback popup renders.
472 476 *
473 477 * Pure function — no side effects. Split out from
474 - * {@see desktop_mode_oauth_render_callback_html()} so unit tests
478 + * {@see openstation_oauth_render_callback_html()} so unit tests
475 479 * can exercise the markup directly without going through a REST
476 480 * dispatch + output-buffer dance.
477 481 *
478 482 * **Why `wp_json_encode` and not `esc_js` for the inlined values.**
@@ -487,16 +491,15 @@
487 491 * neutralising any `</script>` substrings in string values
488 492 * (defence-in-depth — our payload values are server-built, but
489 493 * filters could mutate them).
490 494 *
491 - * @since 0.8.2
492 495 * @internal
493 496 *
494 497 * @param array $payload `{ ok: bool, service?: string, reason?: string, message?: string }`.
495 498 * @return string
496 499 */
497 -function desktop_mode_oauth_build_callback_html( array $payload ) {
498 - // `JSON_HEX_TAG` escapes `<` and `>` as `<` / `>` so a
500 +function openstation_oauth_build_callback_html( array $payload ) {
501 + // `JSON_HEX_TAG` escapes `<` and `>` as `\u003C` / `\u003E` so a
499 502 // `</script>` smuggled into any string value can't terminate the
500 503 // script block early. `JSON_UNESCAPED_SLASHES` keeps URLs
501 504 // readable in DevTools.
502 505 $payload_literal = wp_json_encode( $payload, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES );
@@ -517,9 +520,9 @@
517 520 ( function () {
518 521 try {
519 522 if ( window.opener ) {
520 523 window.opener.postMessage(
521 - { type: 'desktop-mode-oauth-callback', payload: {$payload_literal} },
524 + { type: 'os-oauth-callback', payload: {$payload_literal} },
522 525 {$origin_literal}
523 526 );
524 527 }
525 528 } catch ( e ) {}
@@ -550,15 +553,13 @@
550 553 * The returned `WP_REST_Response` carries the HTML as `data` so
551 554 * unit tests reading `$response->get_data()` still see the body
552 555 * (the filter only fires when the response is actually served).
553 556 *
554 - * @since 0.8.2
555 - *
556 557 * @param array $payload `{ ok: bool, service?: string, reason?: string, message?: string }`.
557 558 * @return WP_REST_Response
558 559 */
559 -function desktop_mode_oauth_render_callback_html( array $payload ) {
560 - $html = desktop_mode_oauth_build_callback_html( $payload );
560 +function openstation_oauth_render_callback_html( array $payload ) {
561 + $html = openstation_oauth_build_callback_html( $payload );
561 562
562 563 $filter_cb = null;
563 564 $filter_cb = static function ( $served, $result, $request ) use ( $html, &$filter_cb ) {
564 565 // Scope tightly to the OAuth callback route — never affect
@@ -577,9 +578,9 @@
577 578 }
578 579 if ( ! headers_sent() ) {
579 580 header( 'Content-Type: text/html; charset=utf-8' );
580 581 }
581 - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- statically-built HTML; embedded payload is esc_js()-escaped during build in `desktop_mode_oauth_build_callback_html`.
582 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- statically-built HTML; embedded payload is wp_json_encode( …, JSON_HEX_TAG )-escaped during build in `openstation_oauth_build_callback_html`.
582 583 echo $html;
583 584 // `true` tells WP_REST_Server we already served the response,
584 585 // short-circuiting the json-encode + `echo` path that follows.
585 586 return true;
@@ -596,13 +597,11 @@
596 597 * The per-relay `capabilities` gate runs in the callback itself
597 598 * so capability denial returns the canonical service-not-allowed
598 599 * error rather than the REST-level "forbidden".
599 600 *
600 - * @since 0.8.2
601 - *
602 601 * @return true|WP_Error
603 602 */
604 -function desktop_mode_rest_oauth_start_permission() {
603 +function openstation_rest_oauth_start_permission() {
605 604 if ( ! is_user_logged_in() ) {
606 605 return new WP_Error(
607 606 'rest_forbidden',
608 607 __( 'You must be logged in to start an OAuth flow.', 'desktop-mode' ),
@@ -614,20 +613,18 @@
614 613
615 614 /**
616 615 * Register the OAuth REST routes on `rest_api_init`.
617 616 *
618 - * @since 0.8.2
619 - *
620 617 * @return void
621 618 */
622 -function desktop_mode_register_oauth_rest_routes() {
619 +function openstation_register_oauth_rest_routes() {
623 620 register_rest_route(
624 621 'desktop-mode/v1',
625 622 '/oauth/start',
626 623 array(
627 624 'methods' => WP_REST_Server::CREATABLE,
628 - 'callback' => 'desktop_mode_rest_oauth_start',
629 - 'permission_callback' => 'desktop_mode_rest_oauth_start_permission',
625 + 'callback' => 'openstation_rest_oauth_start',
626 + 'permission_callback' => 'openstation_rest_oauth_start_permission',
630 627 'args' => array(
631 628 'service' => array(
632 629 'required' => true,
633 630 'type' => 'string',
@@ -640,18 +637,27 @@
640 637 'desktop-mode/v1',
641 638 '/oauth/callback',
642 639 array(
643 640 'methods' => WP_REST_Server::READABLE,
644 - 'callback' => 'desktop_mode_rest_oauth_callback',
641 + 'callback' => 'openstation_rest_oauth_callback',
645 642 // Public — the route is reached via a redirect from the
646 643 // remote service. Auth is the state nonce + (later) the
647 644 // per-service capabilities check on the start side.
648 645 'permission_callback' => '__return_true',
649 646 'args' => array(
650 - 'state' => array( 'required' => true, 'type' => 'string' ),
651 - 'code' => array( 'required' => false, 'type' => 'string' ),
652 - 'error' => array( 'required' => false, 'type' => 'string' ),
647 + 'state' => array(
648 + 'required' => true,
649 + 'type' => 'string',
650 + ),
651 + 'code' => array(
652 + 'required' => false,
653 + 'type' => 'string',
654 + ),
655 + 'error' => array(
656 + 'required' => false,
657 + 'type' => 'string',
658 + ),
653 659 ),
654 660 )
655 661 );
656 662 }
657 -add_action( 'rest_api_init', 'desktop_mode_register_oauth_rest_routes' );
663 +add_action( 'rest_api_init', 'openstation_register_oauth_rest_routes' );