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