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

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

307 lines 10.8 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 ->state(
123 array(
124 'page' => 1,
125 'perPage' => 20,
126 'search' => '',
127 // The role filter: a slug, or `none` for accounts with no
128 // role on this site, as `users.php?role=none` spells it. A
129 // server-side scope, so a group picked on the Roles tab is
130 // a fresh collection rather than a slice of the loaded pages.
131 'role' => '',
132 // The presence filter (All / Online / Active 30d / Never
133 // logged in) — a client-side slice of the page.
134 'status' => '',
135 'orderby' => 'name',
136 'order' => 'asc',
137 // The tab strip: `all` | `add-new` | `edit`.
138 'tab' => 'all',
139 // The Add User form's last failure, for the field it names.
140 'createError' => '',
141 'createField' => '',
142 // Bumped on every successful create — the form resets on it.
143 'created' => 0,
144 )
145 )
146 // A query change replaces the result set — back to the first page.
147 ->action(
148 'filter',
149 static function ( State $state ) {
150 $state->set( 'page', 1 );
151 }
152 )
153 ->action(
154 'page',
155 static function ( State $state, Os $os, array $args ) {
156 $state->set( 'page', max( 1, (int) ( $args['page'] ?? 1 ) ) );
157 }
158 )
159 // A column header click; the table's keys map to the collection's.
160 ->action(
161 'sort',
162 static function ( State $state, Os $os, array $args ) {
163 $orderby = sanitize_key( (string) ( $args['orderby'] ?? 'name' ) );
164 $state->set( 'orderby', in_array( $orderby, SORT_KEYS, true ) ? $orderby : 'name' );
165 $state->set( 'order', 'desc' === strtolower( (string) ( $args['order'] ?? 'asc' ) ) ? 'desc' : 'asc' );
166 }
167 )
168 ->action(
169 'bulk-role',
170 static function ( State $state, Os $os, array $args ) {
171 if ( ! $os->can( 'promote_users' ) ) {
172 $os->toast( __( 'You are not allowed to change roles.', 'desktop-mode' ) );
173 return;
174 }
175 $ids = openstation_users_window_clean_ids( $args['ids'] ?? array() );
176 $result = openstation_users_window_apply_bulk_role( $ids, (string) ( $args['role'] ?? '' ) );
177 $done = report(
178 $os,
179 $result,
180 static function ( array $result ) use ( $ids ) {
181 $ok = ok_count( $result );
182 if ( 0 === $ok ) {
183 return __( 'No users updated.', 'desktop-mode' );
184 }
185 // translators: %1$d users updated, %2$d failed.
186 return sprintf( __( 'Role updated for %1$d user(s) (%2$d skipped).', 'desktop-mode' ), $ok, count( $ids ) - $ok );
187 }
188 );
189 if ( $done ) {
190 $os->announce( 'user', 'updated', $ids );
191 }
192 }
193 )
194 ->action(
195 'bulk-delete',
196 static function ( State $state, Os $os, array $args ) {
197 if ( ! $os->can( is_multisite() ? 'remove_users' : 'delete_users' ) ) {
198 $os->toast( __( 'You are not allowed to delete users.', 'desktop-mode' ) );
199 return;
200 }
201 $ids = openstation_users_window_clean_ids( $args['ids'] ?? array() );
202 $result = openstation_users_window_apply_bulk_delete( $ids, (int) ( $args['reassign'] ?? 0 ) );
203 $done = report(
204 $os,
205 $result,
206 static function ( array $result ) use ( $ids ) {
207 $ok = ok_count( $result );
208 // translators: %1$d users deleted, %2$d skipped.
209 return sprintf( __( '%1$d user(s) deleted (%2$d skipped).', 'desktop-mode' ), $ok, count( $ids ) - $ok );
210 }
211 );
212 if ( $done ) {
213 $os->announce( 'user', 'deleted', $ids );
214 }
215 }
216 )
217 ->action(
218 'send-reset',
219 static function ( State $state, Os $os, array $args ) {
220 report(
221 $os,
222 $os->can( 'edit_users' )
223 ? openstation_users_window_send_password_reset( (int) ( $args['id'] ?? 0 ) )
224 : new \WP_Error( 'openstation_users_forbidden', __( 'You are not allowed to email this user.', 'desktop-mode' ) ),
225 static function ( array $result ) {
226 // translators: %s is the user's email address.
227 return sprintf( __( 'Reset email sent to %s.', 'desktop-mode' ), $result['email'] );
228 }
229 );
230 }
231 )
232 ->action(
233 'resend-welcome',
234 static function ( State $state, Os $os, array $args ) {
235 report(
236 $os,
237 $os->can( 'edit_users' )
238 ? openstation_users_window_resend_welcome( (int) ( $args['id'] ?? 0 ) )
239 : new \WP_Error( 'openstation_users_forbidden', __( 'You are not allowed to email this user.', 'desktop-mode' ) ),
240 static function ( array $result ) {
241 // translators: %s is the user's email address.
242 return sprintf( __( 'Welcome email resent to %s.', 'desktop-mode' ), $result['email'] );
243 }
244 );
245 }
246 )
247 ->action(
248 'create',
249 static function ( State $state, Os $os, array $args ) {
250 $state->set( 'createError', '' );
251 $state->set( 'createField', '' );
252 if ( ! $os->can( 'create_users' ) ) {
253 $state->set( 'createError', __( 'You are not allowed to create users.', 'desktop-mode' ) );
254 return;
255 }
256 $values = is_array( $args['values'] ?? null ) ? $args['values'] : $args;
257 $result = openstation_users_window_create_user( $values );
258 if ( is_wp_error( $result ) ) {
259 $code = $result->get_error_code();
260 $field = '';
261 if ( in_array( $code, array( 'openstation_users_username_exists', 'existing_user_login', 'openstation_users_username_invalid', 'openstation_users_username_required' ), true ) ) {
262 $field = 'username';
263 } elseif ( in_array( $code, array( 'openstation_users_email_exists', 'existing_user_email', 'openstation_users_email_invalid' ), true ) ) {
264 $field = 'email';
265 } elseif ( 'openstation_users_role_forbidden' === $code ) {
266 $field = 'role';
267 }
268 $state->set( 'createError', $result->get_error_message() );
269 $state->set( 'createField', $field );
270 $os->toast( $result->get_error_message() );
271 return;
272 }
273 // translators: %s is the user's email address.
274 $os->toast( sprintf( __( 'User created — welcome email sent to %s.', 'desktop-mode' ), $result['email'] ) );
275 $os->announce( 'user', 'created', array( (int) $result['user_id'] ) );
276 // Back to the list, first page, so the new user shows up.
277 $state->set( 'tab', 'all' );
278 $state->set( 'page', 1 );
279 $state->set( 'created', (int) $state->get( 'created' ) + 1 );
280 }
281 )
282 // A profile saved in the User Edit window, a role changed here, a
283 // user created anywhere: the list repaints (the app's own announces
284 // are skipped by the runtime — the action already returned the rows).
285 ->watch( 'user' )
286 ->data(
287 static function ( State $state ) {
288 $list = openstation_app_rest_page( 'wp/v2/users', list_query( $state ) );
289 // Page out of range — the user was on page 7 and changed the
290 // page size. Land on page 1 rather than paint an empty table.
291 if ( $state->get( 'page' ) > 1 && openstation_app_rest_page_is_out_of_range( $list ) ) {
292 $state->set( 'page', 1 );
293 $list = openstation_app_rest_page( 'wp/v2/users', list_query( $state ) );
294 }
295 // The Content column: the page's counts in two grouped
296 // queries, merged in under the name the REST field uses.
297 $stats = openstation_users_window_stats_for( wp_list_pluck( $list['items'], 'id' ) );
298 foreach ( $list['items'] as $i => $row ) {
299 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
300 if ( isset( $stats[ $id ] ) ) {
301 $list['items'][ $i ]['openstation_user_stats'] = $stats[ $id ];
302 }
303 }
304 return array( 'list' => $list );
305 }
306 );
307