PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
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 0.8.7 All 34 releases
desktop-mode / apps / users / parts / fields.php

fields.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.10, at apps/users/parts/fields.php

313 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Users app — the collection query, the REST fields on the `user`
4 * resource, the role / locale maps, and the page's content stats.
5 *
6 * The app's `data()` reads `wp/v2/users` in-process with the query
7 * {@see openstation_users_window_default_query_args()} shapes, so
8 * every row carries the `openstation_*` fields it asks for exactly as
9 * the browser would have received them. The content counts are NOT
10 * asked for per row: `openstation_users_window_stats_for()` computes
11 * a whole page in two grouped queries and `data()` merges them in
12 * (the `openstation_user_stats` REST field stays registered for other
13 * consumers, at its per-row cost).
14 *
15 * @package OpenStation
16 */
17
18 defined( 'ABSPATH' ) || exit;
19
20 /**
21 * Default REST query args for the Users list.
22 *
23 * @return array
24 */
25 function openstation_users_window_default_query_args() {
26 $args = array(
27 // `_fields` whitelists the columns we render plus the REST
28 // fields registered below. Skipping the whitelist would pull
29 // every meta + every embedded link the user controller emits.
30 '_fields' =>
31 'id,name,slug,email,roles,registered_date,avatar_urls,'
32 . 'openstation_last_login,openstation_presence,openstation_can_edit',
33 // `context=edit` is required because `email`, `roles`, and
34 // `registered_date` are edit-context-only on `/wp/v2/users`.
35 // The window is already gated on `list_users`, the cap
36 // `context=edit` requires.
37 'context' => 'edit',
38 'per_page' => 20,
39 );
40
41 /**
42 * Filter the default outbound REST query args for the Users window.
43 *
44 * @param array $args Default args.
45 */
46 return (array) apply_filters( 'openstation_users_window_query_args', $args );
47 }
48
49 /**
50 * Build the `{ slug: label }` map for every role on the install.
51 *
52 * @return array<string,string>
53 */
54 function openstation_users_window_all_roles_map() {
55 $roles = wp_roles();
56 $map = array();
57 foreach ( (array) $roles->roles as $slug => $info ) {
58 $map[ (string) $slug ] = isset( $info['name'] )
59 ? translate_user_role( (string) $info['name'] )
60 : (string) $slug;
61 }
62 return $map;
63 }
64
65 /**
66 * Build the `{ slug: label }` map for roles the viewer is allowed
67 * to assign. Empty when the viewer lacks `promote_users`.
68 *
69 * @param int $viewer_id Viewer's user id.
70 * @return array<string,string>
71 */
72 function openstation_users_window_role_label_map( $viewer_id ) {
73 $slugs = openstation_users_window_assignable_roles( (int) $viewer_id );
74 if ( empty( $slugs ) ) {
75 return array();
76 }
77 $all = openstation_users_window_all_roles_map();
78 $out = array();
79 foreach ( $slugs as $slug ) {
80 if ( isset( $all[ $slug ] ) ) {
81 $out[ $slug ] = $all[ $slug ];
82 }
83 }
84 return $out;
85 }
86
87 /**
88 * Build the `[ slug => label ]` map for the Add User locale picker.
89 *
90 * Site default is keyed under `''` (empty string) so the form can
91 * reflect "Site default — English (United States)" as the default
92 * choice without forcing the user to know which slug to send.
93 *
94 * @return array<string,string>
95 */
96 function openstation_users_window_locales_map() {
97 $out = array(
98 '' => sprintf(
99 // translators: %s is the site's current locale (e.g. "en_US").
100 __( 'Site default — %s', 'desktop-mode' ),
101 get_locale()
102 ),
103 );
104 if ( ! function_exists( 'get_available_languages' ) ) {
105 require_once ABSPATH . 'wp-admin/includes/translation-install.php';
106 }
107 $languages = (array) get_available_languages();
108 foreach ( $languages as $slug ) {
109 $out[ (string) $slug ] = (string) $slug;
110 }
111 // Always offer en_US even if no .mo file is installed — core
112 // always treats it as available.
113 if ( ! isset( $out['en_US'] ) ) {
114 $out['en_US'] = 'en_US';
115 }
116 return $out;
117 }
118
119 /**
120 * The published post / page and approved comment counts of a page of
121 * users, in two grouped queries — the same numbers
122 * `count_user_posts( $id, $type, true )` and a `get_comments` COUNT
123 * give per user, without the three round trips per row.
124 *
125 * @param int[] $ids User ids.
126 * @return array<int,array{posts:int,pages:int,comments:int}> Keyed by user id; every id present.
127 */
128 function openstation_users_window_stats_for( array $ids ) {
129 global $wpdb;
130 $ids = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) );
131 $out = array();
132 foreach ( $ids as $id ) {
133 $out[ $id ] = array(
134 'posts' => 0,
135 'pages' => 0,
136 'comments' => 0,
137 );
138 }
139 if ( array() === $ids ) {
140 return $out;
141 }
142 $in = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
143 $types = post_type_exists( 'page' ) ? array( 'post', 'page' ) : array( 'post' );
144 $rows = $wpdb->get_results(
145 $wpdb->prepare(
146 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- `$in` is a list of `%d` placeholders.
147 "SELECT post_author, post_type, COUNT(*) AS cnt FROM {$wpdb->posts} WHERE post_author IN ( $in ) AND post_status = 'publish' AND post_type IN ( " . implode( ',', array_fill( 0, count( $types ), '%s' ) ) . ' ) GROUP BY post_author, post_type',
148 array_merge( $ids, $types )
149 ),
150 ARRAY_A
151 );
152 foreach ( (array) $rows as $row ) {
153 $author = (int) $row['post_author'];
154 $key = 'page' === $row['post_type'] ? 'pages' : 'posts';
155 if ( isset( $out[ $author ] ) ) {
156 $out[ $author ][ $key ] = (int) $row['cnt'];
157 }
158 }
159 $rows = $wpdb->get_results(
160 $wpdb->prepare(
161 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- `$in` is a list of `%d` placeholders.
162 "SELECT user_id, COUNT(*) AS cnt FROM {$wpdb->comments} WHERE user_id IN ( $in ) AND comment_approved = '1' GROUP BY user_id",
163 $ids
164 ),
165 ARRAY_A
166 );
167 foreach ( (array) $rows as $row ) {
168 $user = (int) $row['user_id'];
169 if ( isset( $out[ $user ] ) ) {
170 $out[ $user ]['comments'] = (int) $row['cnt'];
171 }
172 }
173 return $out;
174 }
175
176 /**
177 * Register the Users REST fields on the `user` resource.
178 *
179 * Fields:
180 *
181 * - openstation_user_stats — `{ posts: int, pages: int, comments: int }`
182 * - openstation_last_login — UTC unix timestamp, or null when never
183 * - openstation_presence — 'online' | 'inactive' | 'offline'
184 * - openstation_can_edit — viewer can edit / promote this row
185 * - openstation_assignable_roles — role slugs the viewer can assign to this row
186 *
187 * The fields register on every REST request (the `user` resource is
188 * partially public — published authors are visible to anyone), so
189 * `openstation_last_login` and `openstation_presence` gate on
190 * `list_users` (or self) inside their callbacks; `openstation_user_stats`
191 * stays open because it only counts published content. The list asks
192 * for the first three only; the stats and the assignable roles are a
193 * query per row, and the app has both cheaper (a grouped page of
194 * stats, the viewer's assignable roles in the config extra).
195 */
196 function openstation_users_window_register_rest_fields() {
197 $readonly = static function ( $description, $type, $extra = array() ) {
198 return array_merge(
199 array(
200 'description' => $description,
201 'type' => $type,
202 'context' => array( 'view', 'edit', 'embed' ),
203 'readonly' => true,
204 ),
205 $extra
206 );
207 };
208
209 register_rest_field(
210 'user',
211 'openstation_user_stats',
212 array(
213 'get_callback' => static function ( $row ) {
214 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
215 if ( $id <= 0 ) {
216 return array(
217 'posts' => 0,
218 'pages' => 0,
219 'comments' => 0,
220 );
221 }
222 return array(
223 'posts' => (int) count_user_posts( $id, 'post', true ),
224 'pages' => post_type_exists( 'page' ) ? (int) count_user_posts( $id, 'page', true ) : 0,
225 'comments' => (int) get_comments(
226 array(
227 'user_id' => $id,
228 'count' => true,
229 'status' => 'approve',
230 )
231 ),
232 );
233 },
234 'schema' => $readonly( __( 'Per-user content stats: published post / page / comment counts.', 'desktop-mode' ), 'object' ),
235 )
236 );
237
238 register_rest_field(
239 'user',
240 'openstation_last_login',
241 array(
242 'get_callback' => static function ( $row ) {
243 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
244 // Last-login time is sensitive. Only viewers who can see
245 // the Users list — or the user themselves — get it.
246 if ( $id <= 0 || ( get_current_user_id() !== $id && ! current_user_can( 'list_users' ) ) ) {
247 return null;
248 }
249 $ts = (int) get_user_meta( $id, OPENSTATION_LAST_LOGIN_META_KEY, true );
250 return $ts > 0 ? $ts : null;
251 },
252 'schema' => $readonly( __( 'UTC unix timestamp of this user’s last successful login, or null when never recorded.', 'desktop-mode' ), array( 'integer', 'null' ) ),
253 )
254 );
255
256 register_rest_field(
257 'user',
258 'openstation_presence',
259 array(
260 'get_callback' => static function ( $row ) {
261 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
262 if ( $id <= 0 || ! function_exists( 'openstation_presence_status_for_user' ) ) {
263 return 'offline';
264 }
265 // Live presence is sensitive — same gate as last login.
266 if ( get_current_user_id() !== $id && ! current_user_can( 'list_users' ) ) {
267 return 'offline';
268 }
269 return (string) openstation_presence_status_for_user( $id );
270 },
271 'schema' => $readonly(
272 __( 'Live presence status: online / inactive / offline.', 'desktop-mode' ),
273 'string',
274 array( 'enum' => array( 'online', 'inactive', 'offline' ) )
275 ),
276 )
277 );
278
279 register_rest_field(
280 'user',
281 'openstation_can_edit',
282 array(
283 'get_callback' => static function ( $row ) {
284 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
285 $viewer = (int) get_current_user_id();
286 return $id > 0 && $viewer > 0 && (bool) user_can( $viewer, 'edit_user', $id );
287 },
288 'schema' => $readonly( __( 'Whether the requester can edit this user.', 'desktop-mode' ), 'boolean' ),
289 )
290 );
291
292 register_rest_field(
293 'user',
294 'openstation_assignable_roles',
295 array(
296 'get_callback' => static function ( $row ) {
297 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
298 $viewer = (int) get_current_user_id();
299 if ( $id <= 0 || $viewer <= 0 ) {
300 return array();
301 }
302 return array_values( openstation_users_window_assignable_roles( $viewer, $id ) );
303 },
304 'schema' => $readonly(
305 __( 'Role slugs the requester can assign to this user.', 'desktop-mode' ),
306 'array',
307 array( 'items' => array( 'type' => 'string' ) )
308 ),
309 )
310 );
311 }
312 add_action( 'rest_api_init', 'openstation_users_window_register_rest_fields' );
313