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 / includes / my-wordpress / user-list-fields.php

user-list-fields.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.8, at includes/my-wordpress/user-list-fields.php

279 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — My WordPress: per-user REST fields for the
4 * Users-folder list view.
5 *
6 * Surfaces a small "summary" payload on every `/wp/v2/users` row so
7 * the My WordPress folder window can paint a rich tile (avatar +
8 * name + role chip + post count + member-since) without firing
9 * N+1 dossier requests as the user scrolls.
10 *
11 * Fields:
12 *
13 * - `openstation_summary.postCount` int count of non-trash posts authored
14 * - `openstation_summary.roleLabels` array translated role labels (gated on `list_users` OR self)
15 * - `openstation_summary.registered` string ISO-8601 registered date (gated on `list_users` OR self)
16 * - `openstation_summary.lastActive` string ISO-8601 of latest published post, or '' when unknown
17 *
18 * Each field is independently capability-gated so a viewer without
19 * `list_users` still sees `postCount` (public counts) and a public
20 * `lastActive` derived from publish dates, but doesn't see
21 * `roleLabels` or `registered` (private).
22 *
23 * @package OpenStation
24 */
25
26 defined( 'ABSPATH' ) || exit;
27
28 /**
29 * The post types a summary counts. Kept in one place so the per-user
30 * query and the grouped prefetch below can't drift apart — a tile
31 * whose count changes depending on how the page was loaded is worse
32 * than no count at all.
33 *
34 * @return string[]
35 */
36 function openstation_my_wordpress_user_summary_post_types() {
37 return array( 'post', 'page' );
38 }
39
40 /**
41 * Per-request store of prefetched post counts and last-active dates,
42 * keyed by user id. Populated by
43 * {@see openstation_my_wordpress_user_summary_prime()}; read by the
44 * payload builder below.
45 *
46 * @param array|null $write Rows to merge in, or null to read.
47 * @return array<int,array{postCount:int,lastActive:string}>
48 */
49 function &openstation_my_wordpress_user_summary_cache( $write = null ) {
50 static $cache = array();
51
52 if ( is_array( $write ) ) {
53 $cache = $cache + $write;
54 }
55
56 return $cache;
57 }
58
59 /**
60 * Prefetch the two queried facts — authored-post count and latest
61 * publish date — for a whole page of users in two grouped queries.
62 *
63 * Without this, a list view pays `count_user_posts()` plus a
64 * `MAX(post_date_gmt)` lookup *per row*: 200 uncached queries for a
65 * 100-row page. Call this once with the page's ids and the payload
66 * builder answers from memory.
67 *
68 * @param int[] $user_ids User ids on the page.
69 * @return void
70 */
71 function openstation_my_wordpress_user_summary_prime( $user_ids ) {
72 global $wpdb;
73
74 $cached = openstation_my_wordpress_user_summary_cache();
75 $ids = array();
76 foreach ( (array) $user_ids as $user_id ) {
77 $user_id = (int) $user_id;
78 if ( $user_id > 0 && ! isset( $cached[ $user_id ] ) ) {
79 $ids[ $user_id ] = true;
80 }
81 }
82
83 $ids = array_keys( $ids );
84 if ( empty( $ids ) ) {
85 return;
86 }
87
88 $types = openstation_my_wordpress_user_summary_post_types();
89 $id_slots = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
90 $type_slots = implode( ',', array_fill( 0, count( $types ), '%s' ) );
91
92 $rows = array();
93 foreach ( $ids as $user_id ) {
94 $rows[ $user_id ] = array(
95 'postCount' => 0,
96 'lastActive' => '',
97 );
98 }
99
100 // The count `count_user_posts( $id, $types, true )` would produce,
101 // for every id at once. Its `$public_only = true` resolves — via
102 // `get_posts_by_author_sql()` — to exactly `post_status =
103 // 'publish'`, so that is what this reproduces.
104 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- placeholder lists are built from counts, values are passed through prepare().
105 $counts = $wpdb->get_results(
106 $wpdb->prepare(
107 "SELECT post_author, COUNT(*) AS total FROM {$wpdb->posts}
108 WHERE post_author IN ( {$id_slots} )
109 AND post_type IN ( {$type_slots} )
110 AND post_status = 'publish'
111 GROUP BY post_author",
112 array_merge( $ids, $types )
113 )
114 );
115
116 $actives = $wpdb->get_results(
117 $wpdb->prepare(
118 "SELECT post_author, MAX(post_date_gmt) AS latest FROM {$wpdb->posts}
119 WHERE post_author IN ( {$id_slots} )
120 AND post_status = 'publish'
121 AND post_type IN ( {$type_slots} )
122 GROUP BY post_author",
123 array_merge( $ids, $types )
124 )
125 );
126 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
127
128 foreach ( (array) $counts as $row ) {
129 $user_id = (int) $row->post_author;
130 if ( isset( $rows[ $user_id ] ) ) {
131 $rows[ $user_id ]['postCount'] = (int) $row->total;
132 }
133 }
134
135 foreach ( (array) $actives as $row ) {
136 $user_id = (int) $row->post_author;
137 if ( isset( $rows[ $user_id ] ) && $row->latest ) {
138 $rows[ $user_id ]['lastActive'] = (string) mysql2date( 'c', $row->latest, false );
139 }
140 }
141
142 openstation_my_wordpress_user_summary_cache( $rows );
143 }
144
145 /**
146 * Compute the summary payload for one user.
147 *
148 * Answers from the prefetch when
149 * {@see openstation_my_wordpress_user_summary_prime()} has run for
150 * this id, and falls back to the two per-user queries when it hasn't —
151 * so a single-row caller stays correct without having to know about
152 * the batch path.
153 *
154 * @param int $user_id User id.
155 * @return array{postCount:int,roleLabels:array<int,string>,registered:string,lastActive:string}
156 */
157 function openstation_my_wordpress_user_summary_payload( $user_id ) {
158 $user_id = (int) $user_id;
159 if ( $user_id <= 0 ) {
160 return array(
161 'postCount' => 0,
162 'roleLabels' => array(),
163 'registered' => '',
164 'lastActive' => '',
165 );
166 }
167
168 $user = get_userdata( $user_id );
169 if ( ! $user ) {
170 return array(
171 'postCount' => 0,
172 'roleLabels' => array(),
173 'registered' => '',
174 'lastActive' => '',
175 );
176 }
177
178 $can_see_private = current_user_can( 'list_users' )
179 || ( get_current_user_id() === $user_id );
180
181 $primed = openstation_my_wordpress_user_summary_cache();
182 $types = openstation_my_wordpress_user_summary_post_types();
183
184 // `count_user_posts` accepts an array of post types since 4.5; we
185 // limit to public ones the dossier already counts so the tile and
186 // the dossier agree.
187 $post_count = isset( $primed[ $user_id ] )
188 ? (int) $primed[ $user_id ]['postCount']
189 : (int) count_user_posts( $user_id, $types, true );
190
191 $role_labels = array();
192 if ( $can_see_private && function_exists( 'wp_roles' ) ) {
193 $wp_roles = wp_roles();
194 foreach ( (array) $user->roles as $slug ) {
195 $role_labels[] = isset( $wp_roles->role_names[ $slug ] )
196 ? translate_user_role( $wp_roles->role_names[ $slug ] )
197 : $slug;
198 }
199 }
200
201 $registered = '';
202 if ( $can_see_private && '' !== $user->user_registered ) {
203 $registered = mysql2date( 'c', $user->user_registered, false );
204 }
205
206 // Latest published post — useful at-a-glance "active recently?"
207 // signal for the tile. Drawn from `post_date_gmt` so timezone
208 // drift can't shift the displayed date.
209 if ( isset( $primed[ $user_id ] ) ) {
210 $last_active = (string) $primed[ $user_id ]['lastActive'];
211 } else {
212 global $wpdb;
213
214 $type_slots = implode( ',', array_fill( 0, count( $types ), '%s' ) );
215 $last_active_raw = $wpdb->get_var(
216 $wpdb->prepare(
217 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- placeholder list is built from a count, values pass through prepare().
218 "SELECT MAX(post_date_gmt) FROM {$wpdb->posts}
219 WHERE post_author = %d
220 AND post_status = 'publish'
221 AND post_type IN ( {$type_slots} )",
222 array_merge( array( $user_id ), $types )
223 )
224 );
225 $last_active = $last_active_raw ? mysql2date( 'c', $last_active_raw, false ) : '';
226 }
227
228 return array(
229 'postCount' => $post_count,
230 'roleLabels' => $role_labels,
231 'registered' => (string) $registered,
232 'lastActive' => (string) $last_active,
233 );
234 }
235
236 /**
237 * Register the field on the `user` REST resource. The field is
238 * read-only (no `update_callback`); `get_callback` receives the
239 * user response array, from which we read `id`.
240 */
241 function openstation_my_wordpress_register_user_summary_field() {
242 register_rest_field(
243 'user',
244 'openstation_summary',
245 array(
246 'get_callback' => static function ( $user ) {
247 $id = isset( $user['id'] ) ? (int) $user['id'] : 0;
248 return openstation_my_wordpress_user_summary_payload( $id );
249 },
250 'schema' => array(
251 'description' => __( 'Compact user summary for the WP Explorer window.', 'desktop-mode' ),
252 'type' => 'object',
253 'context' => array( 'view', 'edit', 'embed' ),
254 'readonly' => true,
255 'properties' => array(
256 'postCount' => array(
257 'type' => 'integer',
258 'description' => __( 'Count of posts and pages authored by this user.', 'desktop-mode' ),
259 ),
260 'roleLabels' => array(
261 'type' => 'array',
262 'description' => __( 'Translated role labels visible to viewers with list_users.', 'desktop-mode' ),
263 'items' => array( 'type' => 'string' ),
264 ),
265 'registered' => array(
266 'type' => 'string',
267 'description' => __( 'ISO-8601 registration date (gated on list_users).', 'desktop-mode' ),
268 ),
269 'lastActive' => array(
270 'type' => 'string',
271 'description' => __( 'ISO-8601 of latest publish; empty when the user has none.', 'desktop-mode' ),
272 ),
273 ),
274 ),
275 )
276 );
277 }
278 add_action( 'rest_api_init', 'openstation_my_wordpress_register_user_summary_field' );
279