| 1 |
<?php |
| 2 |
/** |
| 3 |
* OpenStation — Native Users Window: capability gates. |
| 4 |
* |
| 5 |
* Multi-tier gating, parallel to WordPress core's `users.php` flow: |
| 6 |
* |
| 7 |
* - `list_users` → REGISTER the window. Cap-only check; the |
| 8 |
* opt-in toggle is JS-side. |
| 9 |
* - `edit_users` → mutation quick-actions (Send password reset, |
| 10 |
* Resend welcome). |
| 11 |
* - `promote_users` → bulk role-change action; per-target gated |
| 12 |
* through {@see openstation_users_window_assignable_roles()}. |
| 13 |
* - `create_users` → "Add new user" toolbar button. |
| 14 |
* - `delete_users` → bulk-delete (single-site). |
| 15 |
* - `remove_users` → bulk-remove (multisite — removes from current site, |
| 16 |
* leaves the network user record alone). |
| 17 |
* |
| 18 |
* UI-side gating is purely UX polish — the REST routes in `rest.php` |
| 19 |
* re-validate every cap and every per-target permission before |
| 20 |
* mutating anything. |
| 21 |
* |
| 22 |
* @package OpenStation |
| 23 |
*/ |
| 24 |
|
| 25 |
defined( 'ABSPATH' ) || exit; |
| 26 |
|
| 27 |
/** |
| 28 |
* Whether the user is eligible to have the Users window registered. |
| 29 |
* |
| 30 |
* @param int|null $user_id Optional. Defaults to `get_current_user_id()`. |
| 31 |
* @return bool |
| 32 |
*/ |
| 33 |
function openstation_users_window_user_can_register( $user_id = null ) { |
| 34 |
$user_id = null === $user_id ? get_current_user_id() : (int) $user_id; |
| 35 |
$can = $user_id > 0 && user_can( $user_id, 'list_users' ); |
| 36 |
|
| 37 |
/** |
| 38 |
* Filter whether the current user can have the Users window |
| 39 |
* registered. This is the boot-time check; runtime "should the |
| 40 |
* dock click use the native window?" is the JS-side |
| 41 |
* `nativeUsersEnabled` flag. |
| 42 |
* |
| 43 |
* @param bool $can Default: `list_users` capability. |
| 44 |
* @param int $user_id User being checked. |
| 45 |
*/ |
| 46 |
return (bool) apply_filters( |
| 47 |
'openstation_users_window_user_can_register', |
| 48 |
$can, |
| 49 |
$user_id |
| 50 |
); |
| 51 |
} |
| 52 |
|
| 53 |
/** |
| 54 |
* Combined cap-and-opt-in check. Used by callers that want the |
| 55 |
* combined answer (e.g. analytics, an arrange-menu entry). |
| 56 |
* |
| 57 |
* @param int|null $user_id Optional. |
| 58 |
* @return bool |
| 59 |
*/ |
| 60 |
function openstation_users_window_user_can_use( $user_id = null ) { |
| 61 |
$user_id = null === $user_id ? get_current_user_id() : (int) $user_id; |
| 62 |
|
| 63 |
$cap_ok = openstation_users_window_user_can_register( $user_id ); |
| 64 |
|
| 65 |
$opt_in = false; |
| 66 |
if ( $cap_ok && function_exists( 'openstation_get_os_settings' ) ) { |
| 67 |
$settings = openstation_get_os_settings( $user_id ); |
| 68 |
$opt_in = ! empty( $settings['nativeUsersEnabled'] ); |
| 69 |
} |
| 70 |
|
| 71 |
$can = $cap_ok && $opt_in; |
| 72 |
|
| 73 |
/** |
| 74 |
* Filter whether the current user has opted into the native Users |
| 75 |
* experience. |
| 76 |
* |
| 77 |
* @param bool $can Default gate result. |
| 78 |
* @param int $user_id User being checked. |
| 79 |
*/ |
| 80 |
return (bool) apply_filters( 'openstation_users_window_user_can_use', $can, $user_id ); |
| 81 |
} |
| 82 |
|
| 83 |
/** |
| 84 |
* Resolve the role slugs the current viewer is allowed to assign to |
| 85 |
* the given target user. |
| 86 |
* |
| 87 |
* Honors core's `editable_roles` filter. Note core's default returns |
| 88 |
* EVERY registered role (administrator included) to any user with |
| 89 |
* `promote_users` — there is no built-in capability-subset hierarchy |
| 90 |
* in core. Sites wanting stricter rules must filter `editable_roles` |
| 91 |
* or `openstation_users_window_assignable_roles` below. We compute |
| 92 |
* the list server-side and surface it on the row so the UI can hide |
| 93 |
* options the viewer can't apply; the REST mutation routes call this |
| 94 |
* same filtered helper and reject anything outside it. |
| 95 |
* |
| 96 |
* @param int $viewer_id Requesting user. |
| 97 |
* @param int $target_id Target user (optional — used by filters). |
| 98 |
* @return string[] Role slugs the viewer can assign to the target. |
| 99 |
*/ |
| 100 |
function openstation_users_window_assignable_roles( $viewer_id, $target_id = 0 ) { |
| 101 |
$viewer_id = (int) $viewer_id; |
| 102 |
if ( $viewer_id <= 0 || ! user_can( $viewer_id, 'promote_users' ) ) { |
| 103 |
return array(); |
| 104 |
} |
| 105 |
|
| 106 |
// Switch to the viewer's perspective so `current_user_can` and |
| 107 |
// `get_editable_roles` evaluate against their caps, not whoever |
| 108 |
// happens to be acting at REST-init time. |
| 109 |
$prev_user = get_current_user_id(); |
| 110 |
$switched = false; |
| 111 |
if ( $prev_user !== $viewer_id ) { |
| 112 |
wp_set_current_user( $viewer_id ); |
| 113 |
$switched = true; |
| 114 |
} |
| 115 |
|
| 116 |
// `get_editable_roles()` lives in wp-admin/includes/user.php |
| 117 |
// which is NOT auto-loaded by the time `init` fires (the hook |
| 118 |
// our window registers on). Without this require_once the |
| 119 |
// function doesn't exist, the array comes back empty, and the |
| 120 |
// admin sees only the site's default_role ("subscriber") in |
| 121 |
// the role dropdown — exactly the symptom that surfaced once |
| 122 |
// real-world testing started. |
| 123 |
if ( ! function_exists( 'get_editable_roles' ) ) { |
| 124 |
require_once ABSPATH . 'wp-admin/includes/user.php'; |
| 125 |
} |
| 126 |
$editable = function_exists( 'get_editable_roles' ) |
| 127 |
? (array) get_editable_roles() |
| 128 |
: array(); |
| 129 |
|
| 130 |
if ( $switched ) { |
| 131 |
wp_set_current_user( $prev_user ); |
| 132 |
} |
| 133 |
|
| 134 |
$slugs = array_keys( $editable ); |
| 135 |
|
| 136 |
/** |
| 137 |
* Filter the role slugs assignable by `$viewer_id` to `$target_id`. |
| 138 |
* |
| 139 |
* Use this to LOCK DOWN role assignment further (e.g. "site |
| 140 |
* managers can't promote anyone to administrator even if core |
| 141 |
* would let them"). Returning an empty array fully disables role |
| 142 |
* mutation for the viewer. |
| 143 |
* |
| 144 |
* Returning a SUPERSET widens the REST endpoints too — both the |
| 145 |
* bulk-role route and the create-user route validate the requested |
| 146 |
* role against this same filtered list (see `rest.php`), so only |
| 147 |
* add roles you genuinely intend to make assignable. |
| 148 |
* |
| 149 |
* @param string[] $slugs Default role slug list. |
| 150 |
* @param int $viewer_id |
| 151 |
* @param int $target_id |
| 152 |
*/ |
| 153 |
return (array) apply_filters( |
| 154 |
'openstation_users_window_assignable_roles', |
| 155 |
$slugs, |
| 156 |
$viewer_id, |
| 157 |
$target_id |
| 158 |
); |
| 159 |
} |
| 160 |
|