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 / users-window / window.php

window.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.8, at includes/users-window/window.php

617 lines 22.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — Native Users Window: registration, template, REST fields.
4 *
5 * Mirrors the structure of the Posts/Pages windows
6 * (`includes/posts-window/`, `includes/pages-window/`) adapted for
7 * the user collection — `/wp/v2/users` rather than `/wp/v2/posts`,
8 * a Role/Email column set instead of taxonomies, and a much heavier
9 * permission story (see `permissions.php` + `rest.php`).
10 *
11 * @package WPDesktopMode
12 */
13
14 defined( 'ABSPATH' ) || exit;
15
16 /**
17 * Echoes the native Users window's template body.
18 *
19 * Same toolbar/table/pager structure as the Posts and Pages
20 * windows. Reuses the `data-desktop-mode-posts-*` selectors so the
21 * shared JS bundle binds to the same hooks regardless of window
22 * mode (the namespace is an internal contract — both windows use
23 * the same template machinery).
24 */
25 function desktop_mode_users_window_render_template() {
26 $can_create = current_user_can( 'create_users' );
27 ob_start();
28 ?>
29 <div class="desktop-mode-posts desktop-mode-users" data-desktop-mode-posts-root>
30 <wpd-tabs value="all" class="desktop-mode-users__tabs" data-desktop-mode-users-tabs>
31 <wpd-tab value="all"><?php esc_html_e( 'All users', 'desktop-mode' ); ?></wpd-tab>
32 <?php if ( $can_create ) : ?>
33 <wpd-tab value="add-new"><?php esc_html_e( 'Add new', 'desktop-mode' ); ?></wpd-tab>
34 <?php endif; ?>
35 <?php /* Profile tab — always visible, always shows the
36 CURRENT logged-in user. Other-user row clicks
37 open a separate `desktop-mode-user-edit`
38 window. */ ?>
39 <wpd-tab value="edit" data-desktop-mode-users-edit-tab>
40 <?php esc_html_e( 'Profile', 'desktop-mode' ); ?>
41 </wpd-tab>
42 </wpd-tabs>
43
44 <wpd-tabpanel for="all" class="desktop-mode-posts__panel">
45 <header class="desktop-mode-posts__toolbar" data-desktop-mode-posts-toolbar>
46 <div class="desktop-mode-posts__toolbar-left">
47 <wpd-segmented data-desktop-mode-posts-status value=""></wpd-segmented>
48 <wpd-text-field
49 data-desktop-mode-posts-search
50 placeholder="<?php esc_attr_e( 'Search name, username, email…', 'desktop-mode' ); ?>"
51 ></wpd-text-field>
52 </div>
53 <div class="desktop-mode-posts__toolbar-right" data-desktop-mode-posts-bulk hidden>
54 <span class="desktop-mode-posts__count" data-desktop-mode-posts-count></span>
55 <span class="desktop-mode-posts__bulk-actions" data-desktop-mode-posts-bulk-actions></span>
56 </div>
57 <div class="desktop-mode-posts__toolbar-trailing">
58 <span class="desktop-mode-posts__toolbar-extras" data-desktop-mode-posts-toolbar-extras></span>
59 <wpd-button variant="ghost" data-desktop-mode-posts-refresh title="<?php esc_attr_e( 'Refresh', 'desktop-mode' ); ?>">
60 <span class="dashicons dashicons-update" aria-hidden="true"></span>
61 </wpd-button>
62 <wpd-button variant="primary" data-desktop-mode-posts-new>
63 <span class="dashicons dashicons-plus" aria-hidden="true"></span>
64 <?php esc_html_e( 'Add new', 'desktop-mode' ); ?>
65 </wpd-button>
66 </div>
67 </header>
68 <div class="desktop-mode-posts__body" data-desktop-mode-posts-body>
69 <wpd-table
70 data-desktop-mode-posts-table
71 selectable="multi"
72 sticky-header
73 sticky-columns="1"
74 hover
75 striped
76 bordered
77 loading
78 >
79 <div slot="empty" class="desktop-mode-posts__empty">
80 <span class="dashicons dashicons-admin-users" aria-hidden="true"></span>
81 <p><?php esc_html_e( 'No users found.', 'desktop-mode' ); ?></p>
82 <p class="desktop-mode-posts__empty-hint">
83 <?php esc_html_e( 'Try a different search or change the role filter.', 'desktop-mode' ); ?>
84 </p>
85 </div>
86 </wpd-table>
87 </div>
88 <footer class="desktop-mode-posts__pager" data-desktop-mode-posts-pager>
89 <div class="desktop-mode-posts__pager-meta">
90 <span data-desktop-mode-posts-page-indicator></span>
91 </div>
92 <div class="desktop-mode-posts__pager-nav">
93 <wpd-button variant="ghost" data-desktop-mode-posts-prev disabled>
94 <span class="dashicons dashicons-arrow-left-alt2" aria-hidden="true"></span>
95 <?php esc_html_e( 'Previous', 'desktop-mode' ); ?>
96 </wpd-button>
97 <wpd-button variant="ghost" data-desktop-mode-posts-next disabled>
98 <?php esc_html_e( 'Next', 'desktop-mode' ); ?>
99 <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
100 </wpd-button>
101 <label class="desktop-mode-posts__pager-perpage">
102 <?php esc_html_e( 'Per page', 'desktop-mode' ); ?>
103 <select data-desktop-mode-posts-per-page>
104 <option value="10">10</option>
105 <option value="20" selected>20</option>
106 <option value="50">50</option>
107 <option value="100">100</option>
108 </select>
109 </label>
110 </div>
111 </footer>
112 </wpd-tabpanel>
113
114 <?php if ( $can_create ) : ?>
115 <wpd-tabpanel for="add-new" class="desktop-mode-users__add-panel">
116 <wpd-form
117 data-desktop-mode-users-add-form
118 submit-label="<?php esc_attr_e( 'Add user', 'desktop-mode' ); ?>"
119 reset-label="<?php esc_attr_e( 'Reset', 'desktop-mode' ); ?>"
120 >
121 <div slot="header">
122 <h2><?php esc_html_e( 'Add a new user', 'desktop-mode' ); ?></h2>
123 <p class="desktop-mode-users__form-lede">
124 <?php esc_html_e( 'WordPress will create the account and (optionally) email the user a notification with a link to set their own password.', 'desktop-mode' ); ?>
125 </p>
126 </div>
127
128 <wpd-text-field
129 name="username"
130 label="<?php esc_attr_e( 'Username (required)', 'desktop-mode' ); ?>"
131 placeholder="<?php esc_attr_e( 'e.g. jane.doe', 'desktop-mode' ); ?>"
132 autocomplete="off"
133 required
134 ></wpd-text-field>
135 <wpd-text-field
136 name="email"
137 type="email"
138 label="<?php esc_attr_e( 'Email (required)', 'desktop-mode' ); ?>"
139 placeholder="<?php esc_attr_e( 'jane@example.com', 'desktop-mode' ); ?>"
140 autocomplete="off"
141 required
142 ></wpd-text-field>
143 <wpd-text-field
144 name="first_name"
145 label="<?php esc_attr_e( 'First name', 'desktop-mode' ); ?>"
146 autocomplete="off"
147 ></wpd-text-field>
148 <wpd-text-field
149 name="last_name"
150 label="<?php esc_attr_e( 'Last name', 'desktop-mode' ); ?>"
151 autocomplete="off"
152 ></wpd-text-field>
153 <wpd-text-field
154 name="url"
155 type="url"
156 label="<?php esc_attr_e( 'Website', 'desktop-mode' ); ?>"
157 placeholder="https://example.com"
158 autocomplete="off"
159 full-width
160 ></wpd-text-field>
161 <?php /* The `role` and `locale` selects are declared here
162 so the components upgrade with the rest of the
163 form; the option list (which depends on the
164 viewer's `editable_roles` map and the install's
165 available languages) is appended JS-side in
166 `mountAddUserForm`. */ ?>
167 <wpd-select
168 name="role"
169 label="<?php esc_attr_e( 'Role', 'desktop-mode' ); ?>"
170 ></wpd-select>
171 <wpd-select
172 name="locale"
173 label="<?php esc_attr_e( 'Language', 'desktop-mode' ); ?>"
174 ></wpd-select>
175 <wpd-text-field
176 name="password"
177 type="password"
178 reveal
179 label="<?php esc_attr_e( 'Password', 'desktop-mode' ); ?>"
180 placeholder="<?php esc_attr_e( 'Auto-generated; click Generate to set one.', 'desktop-mode' ); ?>"
181 autocomplete="new-password"
182 full-width
183 ></wpd-text-field>
184 <div class="desktop-mode-users__form-pwd-actions" full-width>
185 <wpd-button
186 variant="ghost"
187 type="button"
188 data-action="generate-password"
189 >
190 <span class="dashicons dashicons-randomize" aria-hidden="true"></span>
191 <?php esc_html_e( 'Generate strong password', 'desktop-mode' ); ?>
192 </wpd-button>
193 <p class="desktop-mode-users__form-hint">
194 <?php esc_html_e( 'Leave blank to let WordPress generate one and email it to the user.', 'desktop-mode' ); ?>
195 </p>
196 </div>
197 <wpd-checkbox-label
198 name="send_notification"
199 label="<?php esc_attr_e( 'Send the new user an email about their account', 'desktop-mode' ); ?>"
200 checked
201 full-width
202 ></wpd-checkbox-label>
203 </wpd-form>
204 </wpd-tabpanel>
205 <?php endif; ?>
206
207 <wpd-tabpanel for="edit" class="desktop-mode-users__edit-panel">
208 <?php
209 /* The Profile tab is hard-wired to the viewer's own user
210 * id — the JS render shell sets the `user-id` attribute
211 * once the window is mounted. The component does the
212 * rest (lazy-loads data, paints sidebar + form +
213 * activity). Other-user editing happens in the dedicated
214 * `desktop-mode-user-edit` window via row-click. */
215 ?>
216 <wpd-user-profile data-wpd-user-profile-self></wpd-user-profile>
217 </wpd-tabpanel>
218 </div>
219 <?php
220 $html = (string) ob_get_clean();
221
222 /**
223 * Filter the native Users window's template HTML.
224 *
225 * @param string $html Default template HTML.
226 */
227 $filtered = (string) apply_filters( 'desktop_mode_users_window_template_html', $html );
228
229 if ( function_exists( 'desktop_mode_kses_native_window_template' ) ) {
230 echo desktop_mode_kses_native_window_template( $filtered ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper kses-escapes.
231 } else {
232 // Backwards-compatible fallback for the rare case the helper
233 // isn't loaded yet (e.g. a plugin echoing the template
234 // outside the framework's lifecycle).
235 echo wp_kses( $filtered, wp_kses_allowed_html( 'post' ) );
236 }
237 }
238
239 /**
240 * Register the native Users window on `init` (priority 20).
241 */
242 function desktop_mode_users_window_register_window() {
243 if ( ! desktop_mode_users_window_user_can_register() ) {
244 return;
245 }
246
247 $viewer_id = (int) get_current_user_id();
248
249 $window_args = array(
250 'title' => __( 'Users', 'desktop-mode' ),
251 'icon' => 'dashicons-admin-users',
252 'template' => 'desktop_mode_users_window_render_template',
253 // Reuse the Posts bundle — same script + style handles. The
254 // shared module branches on `cfg.mode` to render the Users
255 // view.
256 'script' => 'desktop-mode-posts-window',
257 'style' => 'desktop-mode-posts-window',
258 'width' => 1100,
259 'height' => 720,
260 'min_width' => 720,
261 'min_height' => 480,
262 'placement' => 'none',
263 'config' => array(
264 'mode' => 'users',
265 'introSlug' => 'users',
266 'restRoot' => esc_url_raw( rest_url() ),
267 'restNonce' => wp_create_nonce( 'wp_rest' ),
268 'postsUrl' => esc_url_raw( rest_url( 'wp/v2/users' ) ),
269 'editPostUrlBase' => esc_url_raw( admin_url( 'user-edit.php' ) ),
270 'newPostUrl' => esc_url_raw( admin_url( 'user-new.php' ) ),
271 'usersUrl' => esc_url_raw( rest_url( 'wp/v2/users' ) ),
272 'currentUserId' => $viewer_id,
273 'defaultPerPage' => 20,
274 'queryArgs' => desktop_mode_users_window_default_query_args(),
275 'introSeen' => desktop_mode_has_seen_intro( $viewer_id, 'users' ),
276 'introUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/intros/seen' ) ),
277
278 // Capability flags surfaced to the JS — UI hides actions
279 // the viewer can't perform. Server still re-checks every
280 // mutation, so a tampered flag here changes nothing
281 // security-wise.
282 'canEdit' => current_user_can( 'edit_users' ),
283 'canPromote' => current_user_can( 'promote_users' ),
284 'canCreate' => current_user_can( 'create_users' ),
285 'canDelete' => is_multisite()
286 ? current_user_can( 'remove_users' )
287 : current_user_can( 'delete_users' ),
288 'isMultisite' => is_multisite(),
289
290 // Role list — `{ slug: name }` for every role the viewer
291 // can assign. Empty when the viewer lacks `promote_users`.
292 'assignableRoles' => desktop_mode_users_window_role_label_map( $viewer_id ),
293 // Full role catalog for the role-FILTER dropdown (which
294 // shows EVERY role on the site, even those the viewer
295 // can't assign — they can still filter by them).
296 'allRoles' => desktop_mode_users_window_all_roles_map(),
297
298 // Available site locales for the Add User form's
299 // language dropdown. `'site-default'` = empty string
300 // (the user inherits the site's locale).
301 'locales' => desktop_mode_users_window_locales_map(),
302 'siteLocale' => (string) get_locale(),
303 'defaultRole' => (string) get_option( 'default_role', 'subscriber' ),
304 'createUserUrl' => esc_url_raw(
305 rest_url( 'desktop-mode/v1/users' )
306 ),
307
308 // REST mutation routes — the JS bundle reads these so a
309 // rename or namespace move stays in one place.
310 'bulkRoleUrl' => esc_url_raw(
311 rest_url( 'desktop-mode/v1/users/bulk-role' )
312 ),
313 'bulkDeleteUrl' => esc_url_raw(
314 rest_url( 'desktop-mode/v1/users/bulk-delete' )
315 ),
316 // Profile sub-tab — uses the same config blob to read
317 // the user-edit field option lists, the insights
318 // endpoint base, and the locale/role maps.
319 'insightsUrlBase' => esc_url_raw(
320 rest_url( 'desktop-mode/v1/users/' )
321 ),
322 'contactMethods' => (array) apply_filters(
323 'user_contactmethods',
324 array(),
325 null
326 ),
327 'colorSchemes' => function_exists( 'desktop_mode_user_edit_window_color_schemes' )
328 ? desktop_mode_user_edit_window_color_schemes()
329 : array(),
330 'sendResetUrlBase' => esc_url_raw(
331 rest_url( 'desktop-mode/v1/users/' )
332 ),
333 ),
334 );
335
336 /**
337 * Filter the args used to register the native Users window.
338 *
339 * @param array $window_args Args passed to `desktop_mode_register_window()`.
340 */
341 $window_args = (array) apply_filters( 'desktop_mode_users_window_args', $window_args );
342
343 $registered = desktop_mode_register_window( 'desktop-mode-users', $window_args );
344 if ( is_wp_error( $registered ) ) {
345 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
346 error_log( '[desktop-mode] Native Users window registration failed: ' . $registered->get_error_message() );
347 }
348 }
349 add_action( 'init', 'desktop_mode_users_window_register_window', 20 );
350
351 /**
352 * Default REST query args for the Users window.
353 *
354 * @return array
355 */
356 function desktop_mode_users_window_default_query_args() {
357 $args = array(
358 // `_fields` whitelists the columns we render plus the four
359 // REST fields registered below. Skipping the whitelist would
360 // pull every meta + every embedded link the user controller
361 // emits — heavy on every page change.
362 '_fields' =>
363 'id,name,slug,email,url,description,roles,registered_date,avatar_urls,'
364 . 'desktop_mode_user_stats,desktop_mode_last_login,desktop_mode_presence,'
365 . 'desktop_mode_can_edit,desktop_mode_assignable_roles',
366 // `who=authors` would hide subscribers — we want the full
367 // list. `context=edit` is required because `email`, `roles`,
368 // and `registered_date` are edit-context-only on
369 // `/wp/v2/users`; in `view` they're omitted from the response
370 // entirely (independent of `_fields`), which paints the
371 // table as "No role" / empty email / empty registered date.
372 // The window is already gated on `list_users`, the cap
373 // `context=edit` requires, so this is safe.
374 'context' => 'edit',
375 'per_page' => 20,
376 );
377
378 /**
379 * Filter the default outbound REST query args for the Users window.
380 *
381 * @param array $args Default args.
382 */
383 return (array) apply_filters( 'desktop_mode_users_window_query_args', $args );
384 }
385
386 /**
387 * Build the `{ slug: label }` map for every role on the install.
388 *
389 * Used by the Users window's role FILTER (vs. role-CHANGE menu —
390 * see {@see desktop_mode_users_window_role_label_map()} for that).
391 *
392 * @return array<string,string>
393 */
394 function desktop_mode_users_window_all_roles_map() {
395 $roles = wp_roles();
396 $map = array();
397 foreach ( (array) $roles->roles as $slug => $info ) {
398 $map[ (string) $slug ] = isset( $info['name'] )
399 ? translate_user_role( (string) $info['name'] )
400 : (string) $slug;
401 }
402 return $map;
403 }
404
405 /**
406 * Build the `{ slug: label }` map for roles the viewer is allowed
407 * to assign. Empty when the viewer lacks `promote_users`.
408 *
409 * @param int $viewer_id Viewer's user id.
410 * @return array<string,string>
411 */
412 function desktop_mode_users_window_role_label_map( $viewer_id ) {
413 $slugs = desktop_mode_users_window_assignable_roles( (int) $viewer_id );
414 if ( empty( $slugs ) ) {
415 return array();
416 }
417 $all = desktop_mode_users_window_all_roles_map();
418 $out = array();
419 foreach ( $slugs as $slug ) {
420 if ( isset( $all[ $slug ] ) ) {
421 $out[ $slug ] = $all[ $slug ];
422 }
423 }
424 return $out;
425 }
426
427 /**
428 * Register the Users-window REST fields on the `user` resource.
429 *
430 * Fields:
431 *
432 * - desktop_mode_user_stats — `{ posts: int, pages: int, comments: int }`
433 * - desktop_mode_last_login — UTC unix timestamp, or null when never
434 * - desktop_mode_presence — 'online' | 'inactive' | 'offline'
435 * - desktop_mode_can_edit — viewer can edit / promote this row
436 * - desktop_mode_assignable_roles — role slugs the viewer can assign to this row
437 *
438 * Each field returns sensible empty defaults when the viewer lacks
439 * the cap to see the value, so the JS never has to defend against
440 * "field present but null". The fields register on every REST request
441 * (the `user` resource is partially public — published authors are
442 * visible to anyone), so `desktop_mode_last_login` and
443 * `desktop_mode_presence` gate on `list_users` (or self) inside their
444 * callbacks; `desktop_mode_user_stats` stays open because it only
445 * counts published content.
446 */
447 function desktop_mode_users_window_register_rest_fields() {
448 register_rest_field(
449 'user',
450 'desktop_mode_user_stats',
451 array(
452 'get_callback' => static function ( $row ) {
453 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
454 if ( $id <= 0 ) {
455 return array(
456 'posts' => 0,
457 'pages' => 0,
458 'comments' => 0,
459 );
460 }
461 $posts = (int) count_user_posts( $id, 'post', true );
462 $pages = post_type_exists( 'page' )
463 ? (int) count_user_posts( $id, 'page', true )
464 : 0;
465 $comments = (int) get_comments(
466 array(
467 'user_id' => $id,
468 'count' => true,
469 'status' => 'approve',
470 )
471 );
472 return array(
473 'posts' => $posts,
474 'pages' => $pages,
475 'comments' => $comments,
476 );
477 },
478 'schema' => array(
479 'description' => __( 'Per-user content stats: published post / page / comment counts.', 'desktop-mode' ),
480 'type' => 'object',
481 'context' => array( 'view', 'edit', 'embed' ),
482 'readonly' => true,
483 ),
484 )
485 );
486
487 register_rest_field(
488 'user',
489 'desktop_mode_last_login',
490 array(
491 'get_callback' => static function ( $row ) {
492 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
493 if ( $id <= 0 ) {
494 return null;
495 }
496 // Last-login time is sensitive. Only viewers who can see
497 // the Users list — or the user themselves — get the
498 // real value.
499 if ( get_current_user_id() !== $id && ! current_user_can( 'list_users' ) ) {
500 return null;
501 }
502 $ts = (int) get_user_meta( $id, DESKTOP_MODE_LAST_LOGIN_META_KEY, true );
503 return $ts > 0 ? $ts : null;
504 },
505 'schema' => array(
506 'description' => __( 'UTC unix timestamp of this user’s last successful login, or null when never recorded.', 'desktop-mode' ),
507 'type' => array( 'integer', 'null' ),
508 'context' => array( 'view', 'edit', 'embed' ),
509 'readonly' => true,
510 ),
511 )
512 );
513
514 register_rest_field(
515 'user',
516 'desktop_mode_presence',
517 array(
518 'get_callback' => static function ( $row ) {
519 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
520 if ( $id <= 0 || ! function_exists( 'desktop_mode_presence_status_for_user' ) ) {
521 return 'offline';
522 }
523 // Live presence is sensitive. Only viewers who can see
524 // the Users list — or the user themselves — get the
525 // real value.
526 if ( get_current_user_id() !== $id && ! current_user_can( 'list_users' ) ) {
527 return 'offline';
528 }
529 return (string) desktop_mode_presence_status_for_user( $id );
530 },
531 'schema' => array(
532 'description' => __( 'Live presence status: online / inactive / offline.', 'desktop-mode' ),
533 'type' => 'string',
534 'enum' => array( 'online', 'inactive', 'offline' ),
535 'context' => array( 'view', 'edit', 'embed' ),
536 'readonly' => true,
537 ),
538 )
539 );
540
541 register_rest_field(
542 'user',
543 'desktop_mode_can_edit',
544 array(
545 'get_callback' => static function ( $row ) {
546 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
547 $viewer = (int) get_current_user_id();
548 if ( $id <= 0 || $viewer <= 0 ) {
549 return false;
550 }
551 return (bool) user_can( $viewer, 'edit_user', $id );
552 },
553 'schema' => array(
554 'description' => __( 'Whether the requester can edit this user.', 'desktop-mode' ),
555 'type' => 'boolean',
556 'context' => array( 'view', 'edit', 'embed' ),
557 'readonly' => true,
558 ),
559 )
560 );
561
562 register_rest_field(
563 'user',
564 'desktop_mode_assignable_roles',
565 array(
566 'get_callback' => static function ( $row ) {
567 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
568 $viewer = (int) get_current_user_id();
569 if ( $id <= 0 || $viewer <= 0 ) {
570 return array();
571 }
572 return array_values( desktop_mode_users_window_assignable_roles( $viewer, $id ) );
573 },
574 'schema' => array(
575 'description' => __( 'Role slugs the requester can assign to this user.', 'desktop-mode' ),
576 'type' => 'array',
577 'items' => array( 'type' => 'string' ),
578 'context' => array( 'view', 'edit', 'embed' ),
579 'readonly' => true,
580 ),
581 )
582 );
583 }
584 add_action( 'rest_api_init', 'desktop_mode_users_window_register_rest_fields' );
585
586 /**
587 * Build the `[ slug => label ]` map for the Add User locale picker.
588 *
589 * Site default is keyed under `''` (empty string) so the form can
590 * reflect "Site default — English (United States)" as the default
591 * choice without forcing the user to know which slug to send.
592 *
593 * @return array<string,string>
594 */
595 function desktop_mode_users_window_locales_map() {
596 $out = array(
597 '' => sprintf(
598 // translators: %s is the site's current locale (e.g. "en_US").
599 __( 'Site default — %s', 'desktop-mode' ),
600 get_locale()
601 ),
602 );
603 if ( ! function_exists( 'get_available_languages' ) ) {
604 require_once ABSPATH . 'wp-admin/includes/translation-install.php';
605 }
606 $languages = (array) get_available_languages();
607 foreach ( $languages as $slug ) {
608 $out[ (string) $slug ] = (string) $slug;
609 }
610 // Always offer en_US even if no .mo file is installed — core
611 // always treats it as available.
612 if ( ! isset( $out['en_US'] ) ) {
613 $out['en_US'] = 'en_US';
614 }
615 return $out;
616 }
617