PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
1.1.11 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 All 35 releases
desktop-mode / apps / users / users.os.php

users.os.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.11, at apps/users/users.os.php

337 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Users — the Users window, as an OpenStation app.
4 *
5 * Claims the FROZEN id `desktop-mode-users` (see AGENTS.md) so the
6 * `users.php` URL remap, session restores and every hook keep
7 * working. The body is `users.os.ts`, a client view over the rows
8 * `data()` reads from `wp/v2/users` in-process — the same collection,
9 * fields and filterable query, plus a page of content counts in two
10 * grouped queries. The mutations are actions over the functions in
11 * `parts/rest.php`, which the `desktop-mode/v1/users*` routes expose
12 * too. The Profile tab hosts `<os-user-profile>` from the companion
13 * bundle `parts/profile-script.php` registers.
14 *
15 * (Header kept short on purpose: Plugin Check's direct-access scan
16 * reads only the first 50 raw lines, and the guard below must land
17 * inside that window.)
18 *
19 * @package OpenStation
20 */
21
22 namespace OpenStation\Apps\Users;
23
24 use OpenStation\App;
25 use OpenStation\App\Os;
26 use OpenStation\App\State;
27
28 // Direct access, unless a standalone host is booting on bare PHP.
29 if ( ! defined( 'ABSPATH' ) ) {
30 defined( 'OPENSTATION_STANDALONE' ) || exit;
31 }
32
33 require_once __DIR__ . '/parts/permissions.php';
34 require_once __DIR__ . '/parts/login-tracker.php';
35 require_once __DIR__ . '/parts/color-schemes.php';
36 require_once __DIR__ . '/parts/fields.php';
37 require_once __DIR__ . '/parts/roles-summary.php';
38 require_once __DIR__ . '/parts/activity-summary.php';
39 require_once __DIR__ . '/parts/facts.php';
40 require_once __DIR__ . '/parts/rest.php';
41 require_once __DIR__ . '/parts/profile-script.php';
42
43 /** The `orderby` values `wp/v2/users` accepts that the table offers. */
44 const SORT_KEYS = array( 'name', 'registered_date', 'email' );
45
46 /**
47 * The query the list reads — the filterable defaults plus the state.
48 *
49 * @param State $state State.
50 * @return array<string,mixed>
51 */
52 function list_query( State $state ) {
53 $query = openstation_users_window_default_query_args();
54 $query['page'] = max( 1, (int) $state->get( 'page' ) );
55 $query['per_page'] = max( 1, (int) $state->get( 'perPage' ) );
56 $query['orderby'] = (string) $state->get( 'orderby' );
57 $query['order'] = (string) $state->get( 'order' );
58 $search = trim( (string) $state->get( 'search' ) );
59 if ( '' !== $search ) {
60 $query['search'] = $search;
61 }
62 $role = trim( (string) $state->get( 'role' ) );
63 if ( 'none' === $role ) {
64 // As users.php does: the accounts with no role on this site, or nobody.
65 $ids = wp_get_users_with_no_role();
66 $query['include'] = $ids ? array_map( 'intval', $ids ) : array( 0 );
67 } elseif ( '' !== $role ) {
68 $query['roles'] = $role;
69 }
70 return $query;
71 }
72
73 /**
74 * Say what a mutation did.
75 *
76 * @param Os $os Host handle.
77 * @param array|\WP_Error $result The mutation's answer.
78 * @param callable $ok `function ( array $result ): string` — the success message.
79 * @return bool Whether it succeeded.
80 */
81 function report( Os $os, $result, callable $ok ) {
82 if ( is_wp_error( $result ) ) {
83 $os->toast( $result->get_error_message() );
84 return false;
85 }
86 $os->toast( $ok( $result ) );
87 return true;
88 }
89
90 /**
91 * How many of a bulk result's rows succeeded.
92 *
93 * @param array<string,mixed> $result Bulk result.
94 * @return int
95 */
96 function ok_count( array $result ) {
97 return count(
98 array_filter(
99 (array) $result['results'],
100 static function ( $row ) {
101 return ! empty( $row['ok'] );
102 }
103 )
104 );
105 }
106
107 return App::define( 'desktop-mode-users' )
108 ->title( __( 'Users', 'desktop-mode' ) )
109 ->icon( 'dashicons-admin-users' )
110 ->size( 1100, 720 )
111 ->min_size( 720, 480 )
112 // The Users dock tile lives in WordPress's `$menu`; the URL remap
113 // routes its click here when the opt-in is on.
114 ->placement( 'none' )
115 ->can(
116 static function () {
117 return openstation_users_window_user_can_register();
118 }
119 )
120 // Resolved when the window registers, for the viewer registering it.
121 ->config( 'openstation_users_profile_facts' )
122 // The Users menu, while this window is the one answering for it:
123 // the dock's submenu becomes these tabs, same labels, same order,
124 // and picking one opens the window on it. The window's own strip
125 // renders from the same list (`menuTabs` in the config extra), so
126 // the two cannot drift.
127 ->menu(
128 'users.php',
129 static function () {
130 $tabs = array(
131 'all' => array(
132 'label' => __( 'People', 'desktop-mode' ),
133 'page' => 'users.php',
134 ),
135 'roles' => __( 'Roles', 'desktop-mode' ),
136 'activity' => __( 'Activity', 'desktop-mode' ),
137 );
138 if ( current_user_can( 'create_users' ) ) {
139 $tabs['add-new'] = array(
140 'label' => __( 'Add new', 'desktop-mode' ),
141 'page' => 'user-new.php',
142 );
143 }
144 $tabs['edit'] = array(
145 'label' => __( 'Profile', 'desktop-mode' ),
146 'page' => 'profile.php',
147 );
148 return $tabs;
149 },
150 'openstation_users_window_user_can_use'
151 )
152 ->state(
153 array(
154 'page' => 1,
155 'perPage' => 20,
156 'search' => '',
157 // The role filter: a slug, or `none` for accounts with no
158 // role on this site, as `users.php?role=none` spells it. A
159 // server-side scope, so a group picked on the Roles tab is
160 // a fresh collection rather than a slice of the loaded pages.
161 'role' => '',
162 // The presence filter (All / Online / Active 30d / Never
163 // logged in) — a client-side slice of the page.
164 'status' => '',
165 'orderby' => 'name',
166 'order' => 'asc',
167 // The tab strip: `all` | `add-new` | `edit`.
168 'tab' => 'all',
169 // The Add User form's last failure, for the field it names.
170 'createError' => '',
171 'createField' => '',
172 // Bumped on every successful create — the form resets on it.
173 'created' => 0,
174 )
175 )
176 // A query change replaces the result set — back to the first page.
177 ->action(
178 'filter',
179 static function ( State $state ) {
180 $state->set( 'page', 1 );
181 }
182 )
183 ->action(
184 'page',
185 static function ( State $state, Os $os, array $args ) {
186 $state->set( 'page', max( 1, (int) ( $args['page'] ?? 1 ) ) );
187 }
188 )
189 // A column header click; the table's keys map to the collection's.
190 ->action(
191 'sort',
192 static function ( State $state, Os $os, array $args ) {
193 $orderby = sanitize_key( (string) ( $args['orderby'] ?? 'name' ) );
194 $state->set( 'orderby', in_array( $orderby, SORT_KEYS, true ) ? $orderby : 'name' );
195 $state->set( 'order', 'desc' === strtolower( (string) ( $args['order'] ?? 'asc' ) ) ? 'desc' : 'asc' );
196 }
197 )
198 ->action(
199 'bulk-role',
200 static function ( State $state, Os $os, array $args ) {
201 if ( ! $os->can( 'promote_users' ) ) {
202 $os->toast( __( 'You are not allowed to change roles.', 'desktop-mode' ) );
203 return;
204 }
205 $ids = openstation_users_window_clean_ids( $args['ids'] ?? array() );
206 $result = openstation_users_window_apply_bulk_role( $ids, (string) ( $args['role'] ?? '' ) );
207 $done = report(
208 $os,
209 $result,
210 static function ( array $result ) use ( $ids ) {
211 $ok = ok_count( $result );
212 if ( 0 === $ok ) {
213 return __( 'No users updated.', 'desktop-mode' );
214 }
215 // translators: %1$d users updated, %2$d failed.
216 return sprintf( __( 'Role updated for %1$d user(s) (%2$d skipped).', 'desktop-mode' ), $ok, count( $ids ) - $ok );
217 }
218 );
219 if ( $done ) {
220 $os->announce( 'user', 'updated', $ids );
221 }
222 }
223 )
224 ->action(
225 'bulk-delete',
226 static function ( State $state, Os $os, array $args ) {
227 if ( ! $os->can( is_multisite() ? 'remove_users' : 'delete_users' ) ) {
228 $os->toast( __( 'You are not allowed to delete users.', 'desktop-mode' ) );
229 return;
230 }
231 $ids = openstation_users_window_clean_ids( $args['ids'] ?? array() );
232 $result = openstation_users_window_apply_bulk_delete( $ids, (int) ( $args['reassign'] ?? 0 ) );
233 $done = report(
234 $os,
235 $result,
236 static function ( array $result ) use ( $ids ) {
237 $ok = ok_count( $result );
238 // translators: %1$d users deleted, %2$d skipped.
239 return sprintf( __( '%1$d user(s) deleted (%2$d skipped).', 'desktop-mode' ), $ok, count( $ids ) - $ok );
240 }
241 );
242 if ( $done ) {
243 $os->announce( 'user', 'deleted', $ids );
244 }
245 }
246 )
247 ->action(
248 'send-reset',
249 static function ( State $state, Os $os, array $args ) {
250 report(
251 $os,
252 $os->can( 'edit_users' )
253 ? openstation_users_window_send_password_reset( (int) ( $args['id'] ?? 0 ) )
254 : new \WP_Error( 'openstation_users_forbidden', __( 'You are not allowed to email this user.', 'desktop-mode' ) ),
255 static function ( array $result ) {
256 // translators: %s is the user's email address.
257 return sprintf( __( 'Reset email sent to %s.', 'desktop-mode' ), $result['email'] );
258 }
259 );
260 }
261 )
262 ->action(
263 'resend-welcome',
264 static function ( State $state, Os $os, array $args ) {
265 report(
266 $os,
267 $os->can( 'edit_users' )
268 ? openstation_users_window_resend_welcome( (int) ( $args['id'] ?? 0 ) )
269 : new \WP_Error( 'openstation_users_forbidden', __( 'You are not allowed to email this user.', 'desktop-mode' ) ),
270 static function ( array $result ) {
271 // translators: %s is the user's email address.
272 return sprintf( __( 'Welcome email resent to %s.', 'desktop-mode' ), $result['email'] );
273 }
274 );
275 }
276 )
277 ->action(
278 'create',
279 static function ( State $state, Os $os, array $args ) {
280 $state->set( 'createError', '' );
281 $state->set( 'createField', '' );
282 if ( ! $os->can( 'create_users' ) ) {
283 $state->set( 'createError', __( 'You are not allowed to create users.', 'desktop-mode' ) );
284 return;
285 }
286 $values = is_array( $args['values'] ?? null ) ? $args['values'] : $args;
287 $result = openstation_users_window_create_user( $values );
288 if ( is_wp_error( $result ) ) {
289 $code = $result->get_error_code();
290 $field = '';
291 if ( in_array( $code, array( 'openstation_users_username_exists', 'existing_user_login', 'openstation_users_username_invalid', 'openstation_users_username_required' ), true ) ) {
292 $field = 'username';
293 } elseif ( in_array( $code, array( 'openstation_users_email_exists', 'existing_user_email', 'openstation_users_email_invalid' ), true ) ) {
294 $field = 'email';
295 } elseif ( 'openstation_users_role_forbidden' === $code ) {
296 $field = 'role';
297 }
298 $state->set( 'createError', $result->get_error_message() );
299 $state->set( 'createField', $field );
300 $os->toast( $result->get_error_message() );
301 return;
302 }
303 // translators: %s is the user's email address.
304 $os->toast( sprintf( __( 'User created — welcome email sent to %s.', 'desktop-mode' ), $result['email'] ) );
305 $os->announce( 'user', 'created', array( (int) $result['user_id'] ) );
306 // Back to the list, first page, so the new user shows up.
307 $state->set( 'tab', 'all' );
308 $state->set( 'page', 1 );
309 $state->set( 'created', (int) $state->get( 'created' ) + 1 );
310 }
311 )
312 // A profile saved in the User Edit window, a role changed here, a
313 // user created anywhere: the list repaints (the app's own announces
314 // are skipped by the runtime — the action already returned the rows).
315 ->watch( 'user' )
316 ->data(
317 static function ( State $state ) {
318 $list = openstation_app_rest_page( 'wp/v2/users', list_query( $state ) );
319 // Page out of range — the user was on page 7 and changed the
320 // page size. Land on page 1 rather than paint an empty table.
321 if ( $state->get( 'page' ) > 1 && openstation_app_rest_page_is_out_of_range( $list ) ) {
322 $state->set( 'page', 1 );
323 $list = openstation_app_rest_page( 'wp/v2/users', list_query( $state ) );
324 }
325 // The Content column: the page's counts in two grouped
326 // queries, merged in under the name the REST field uses.
327 $stats = openstation_users_window_stats_for( wp_list_pluck( $list['items'], 'id' ) );
328 foreach ( $list['items'] as $i => $row ) {
329 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
330 if ( isset( $stats[ $id ] ) ) {
331 $list['items'][ $i ]['openstation_user_stats'] = $stats[ $id ];
332 }
333 }
334 return array( 'list' => $list );
335 }
336 );
337