*/ public static function abilities(): array { $id = ['minimum' => 1, 'type' => 'integer']; return [ new Ability( 'sarcio/users-list', __('Look at who can sign in', 'sarcio'), Ability::READ, 'The site’s users: id, display name, login, role and when they joined. Never their email or anything secret. Filter by role or search by name.', ['properties' => ['role' => ['maxLength' => 40, 'type' => 'string'], 'search' => ['maxLength' => 100, 'type' => 'string']], 'required' => [], 'type' => 'object'], static fn (array $args): bool => current_user_can('list_users'), static fn (array $args): array => self::list($args), ), new Ability( 'sarcio/user-role-set', __('Change what a user may do', 'sarcio'), Ability::WRITE, 'Give a user another role (subscriber, contributor, author, editor, administrator, or a role a plugin added such as shop_manager). Confirmed by the person; undo gives the previous role back. Your own role cannot be changed here.', ['properties' => ['id' => $id, 'role' => ['maxLength' => 40, 'minLength' => 1, 'type' => 'string']], 'required' => ['id', 'role'], 'type' => 'object'], static fn (array $args): bool => current_user_can('promote_users'), static fn (array $args): array => self::setRole($args), null, static fn (array $before, array $after): array => self::restoreRole($before), true, ), new Ability( 'sarcio/user-invite', __('Invite someone', 'sarcio'), Ability::WRITE, 'Create a user with a role and email them a link to set their password. Confirmed by the person first; cannot be undone from here (remove the user instead). Never make up an email address.', [ 'properties' => [ 'email' => ['format' => 'email', 'maxLength' => 200, 'type' => 'string'], 'name' => ['maxLength' => 100, 'type' => 'string'], 'role' => ['maxLength' => 40, 'minLength' => 1, 'type' => 'string'], ], 'required' => ['email', 'role'], 'type' => 'object', ], static fn (array $args): bool => current_user_can('create_users'), static fn (array $args): array => self::invite($args), null, null, true, ), new Ability( 'sarcio/user-remove', __('Remove a user', 'sarcio'), Ability::WRITE, 'Remove a user; their pages, posts and products are handed to another user (reassignTo, an id from users-list) so nothing is lost. Confirmed by the person first; cannot be undone. Never yourself, never the last administrator.', ['properties' => ['id' => $id, 'reassignTo' => $id], 'required' => ['id', 'reassignTo'], 'type' => 'object'], static fn (array $args): bool => current_user_can('delete_users'), static fn (array $args): array => self::remove($args), null, null, true, ), ]; } public static function describe(object $user): array { return [ 'id' => (int) $user->ID, 'login' => (string) ($user->user_login ?? ''), 'name' => (string) ($user->display_name ?? ''), 'registered' => substr((string) ($user->user_registered ?? ''), 0, 10), 'role' => (string) (is_array($user->roles ?? null) && $user->roles !== [] ? reset($user->roles) : ''), ]; } /** @return array role slug => label */ private static function roles(): array { $roles = function_exists('get_editable_roles') ? get_editable_roles() : (function_exists('wp_roles') ? wp_roles()->roles : []); $out = []; foreach ((array) $roles as $slug => $role) { $out[(string) $slug] = (string) (is_array($role) ? ($role['name'] ?? $slug) : $slug); } return $out; } /** @param array $args */ private static function list(array $args): array { $query = ['number' => 200, 'orderby' => 'display_name']; if (($args['role'] ?? '') !== '') { $query['role'] = sanitize_key((string) $args['role']); } if (($args['search'] ?? '') !== '') { $query['search'] = '*' . sanitize_text_field((string) $args['search']) . '*'; $query['search_columns'] = ['display_name', 'user_login', 'user_nicename']; } $users = get_users($query); return [ 'roles' => self::roles(), 'users' => array_map([self::class, 'describe'], array_values(array_filter(is_array($users) ? $users : [], 'is_object'))), ]; } private static function adminCount(): int { $admins = get_users(['role' => 'administrator', 'fields' => 'ID', 'number' => 50]); return is_array($admins) ? count($admins) : 0; } /** @param array $args */ private static function setRole(array $args): array { $id = (int) $args['id']; $role = sanitize_key((string) $args['role']); $user = get_userdata($id); if (!is_object($user) || !isset($user->ID)) { return ['error' => __('There is no user with that id.', 'sarcio')]; } if ($id === get_current_user_id()) { return ['error' => __('You cannot change your own role here.', 'sarcio')]; } if (!isset(self::roles()[$role])) { return ['error' => __('This site has no role with that name.', 'sarcio')]; } $before = self::describe($user)['role']; if ($before === $role) { return ['error' => __('They already have that role.', 'sarcio')]; } if ($before === 'administrator' && self::adminCount() <= 1) { return ['error' => __('That is the last administrator; give someone else the role first.', 'sarcio')]; } if ($role === 'administrator' && !current_user_can('manage_options')) { return ['error' => __('Only an administrator can make another administrator.', 'sarcio')]; } $result = wp_update_user(['ID' => $id, 'role' => $role]); if (is_wp_error($result)) { return ['error' => sanitize_text_field($result->get_error_message())]; } return [ 'journal' => [ 'after' => ['id' => $id, 'role' => $role], 'before' => ['id' => $id, 'role' => $before], 'summary' => sprintf( /* translators: 1: the user's name, 2: the role. */ __('Made %1$s a %2$s', 'sarcio'), self::describe($user)['name'], self::roles()[$role], ), 'target' => 'user:' . $id, ], 'user' => self::describe(get_userdata($id)), ]; } /** @param array $before */ private static function restoreRole(array $before): array { $id = (int) ($before['id'] ?? 0); $role = (string) ($before['role'] ?? ''); if (!is_object(get_userdata($id)) || $role === '') { return ['error' => __('That user is no longer there.', 'sarcio')]; } $result = wp_update_user(['ID' => $id, 'role' => $role]); return is_wp_error($result) ? ['error' => sanitize_text_field($result->get_error_message())] : ['restored' => true]; } /** @param array $args */ private static function invite(array $args): array { $email = sanitize_email((string) $args['email']); $role = sanitize_key((string) $args['role']); if ($email === '' || !is_email($email)) { return ['error' => __('That is not an email address.', 'sarcio')]; } if (!isset(self::roles()[$role])) { return ['error' => __('This site has no role with that name.', 'sarcio')]; } if ($role === 'administrator' && !current_user_can('manage_options')) { return ['error' => __('Only an administrator can invite another administrator.', 'sarcio')]; } if (email_exists($email)) { return ['error' => __('Someone with that email already has an account here.', 'sarcio')]; } $base = sanitize_user((string) strstr($email, '@', true), true); $login = $base !== '' ? $base : 'user'; for ($n = 2; username_exists($login); $n++) { $login = $base . $n; } $id = wp_insert_user([ 'display_name' => sanitize_text_field((string) ($args['name'] ?? '')) !== '' ? sanitize_text_field((string) $args['name']) : $login, 'role' => $role, 'user_email' => $email, 'user_login' => $login, 'user_pass' => wp_generate_password(24, true), ]); if (is_wp_error($id)) { return ['error' => sanitize_text_field($id->get_error_message())]; } if (function_exists('wp_new_user_notification')) { wp_new_user_notification((int) $id, null, 'user'); } $user = get_userdata((int) $id); return [ 'journal' => [ 'after' => ['id' => (int) $id, 'role' => $role], 'before' => [], 'summary' => sprintf( /* translators: 1: the user's name, 2: the role. */ __('Invited %1$s as a %2$s (cannot be undone from here)', 'sarcio'), is_object($user) ? self::describe($user)['name'] : $login, self::roles()[$role], ), 'target' => 'user:' . $id, ], 'user' => is_object($user) ? self::describe($user) : ['id' => (int) $id], ]; } /** @param array $args */ private static function remove(array $args): array { $id = (int) $args['id']; $to = (int) $args['reassignTo']; $user = get_userdata($id); if (!is_object($user) || !isset($user->ID)) { return ['error' => __('There is no user with that id.', 'sarcio')]; } if ($id === get_current_user_id()) { return ['error' => __('You cannot remove yourself.', 'sarcio')]; } if ($to === $id || !is_object(get_userdata($to))) { return ['error' => __('Say which other user takes over their content.', 'sarcio')]; } $described = self::describe($user); if ($described['role'] === 'administrator' && self::adminCount() <= 1) { return ['error' => __('That is the last administrator.', 'sarcio')]; } require_once ABSPATH . 'wp-admin/includes/user.php'; if (!wp_delete_user($id, $to)) { return ['error' => __('WordPress could not remove that user.', 'sarcio')]; } return [ 'journal' => [ 'after' => ['removed' => true], 'before' => $described + ['reassignedTo' => $to], 'summary' => sprintf( /* translators: %s: the user's name. */ __('Removed the user %s (cannot be undone from here)', 'sarcio'), $described['name'], ), 'target' => 'user:' . $id, ], 'removed' => $described, ]; } }