PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.1
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 / includes / my-wordpress / user-stats.php

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

328 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — My WordPress: per-user stats endpoint.
4 *
5 * `GET /desktop-mode/v1/user-stats/<id>` returns an aggregated
6 * profile + activity blob for the requested user. The right
7 * preview pane in the My WordPress folder uses it to paint a rich
8 * dossier (post / page / comment counts, recent posts, top
9 * categories, role + member-since) without forcing the client to
10 * make N parallel REST calls.
11 *
12 * Permissions: anyone with `list_users` sees full data; everyone
13 * else sees the public subset (display name, avatar, post archive
14 * link, public post counts). Sensitive fields (email, registered
15 * date, role) are gated on the cap.
16 *
17 * @package WPDesktopMode
18 * @since 0.8.0
19 */
20
21 defined( 'ABSPATH' ) || exit;
22
23 /**
24 * Register the route.
25 *
26 * @since 0.8.0
27 */
28 function desktop_mode_my_wordpress_register_user_stats_route() {
29 register_rest_route(
30 'desktop-mode/v1',
31 '/user-stats/(?P<id>\d+)',
32 array(
33 'methods' => WP_REST_Server::READABLE,
34 'callback' => 'desktop_mode_my_wordpress_user_stats_callback',
35 'permission_callback' => static function () {
36 // Logged-in users only — author archives are public,
37 // but the dossier mixes counts that aren't.
38 return is_user_logged_in();
39 },
40 'args' => array(
41 'id' => array(
42 'required' => true,
43 'type' => 'integer',
44 'sanitize_callback' => 'absint',
45 ),
46 ),
47 )
48 );
49 }
50 add_action( 'rest_api_init', 'desktop_mode_my_wordpress_register_user_stats_route' );
51
52 /**
53 * Aggregator callback. Returns the dossier shape (see file
54 * docblock above for fields).
55 *
56 * @since 0.8.0
57 *
58 * @param WP_REST_Request $request REST request.
59 * @return array|WP_Error
60 */
61 function desktop_mode_my_wordpress_user_stats_callback( $request ) {
62 global $wpdb;
63 $user_id = (int) $request->get_param( 'id' );
64 $user = get_userdata( $user_id );
65 if ( ! $user ) {
66 return new WP_Error(
67 'desktop_mode_user_not_found',
68 __( 'User not found.', 'desktop-mode' ),
69 array( 'status' => 404 )
70 );
71 }
72
73 $can_see_private = current_user_can( 'list_users' )
74 || ( get_current_user_id() === $user_id );
75
76 // ----- Profile -----------------------------------------------------
77 $profile = array(
78 'id' => (int) $user->ID,
79 'name' => $user->display_name,
80 'description' => (string) $user->description,
81 'link' => get_author_posts_url( $user->ID ),
82 'website' => esc_url_raw( $user->user_url ),
83 'avatarUrl' => get_avatar_url( $user->ID, array( 'size' => 192 ) ),
84 );
85 if ( $can_see_private ) {
86 $profile['email'] = $user->user_email;
87 $profile['username'] = $user->user_login;
88 $profile['registered'] = mysql2date( 'c', $user->user_registered, false );
89 $profile['roles'] = array_values( (array) $user->roles );
90 $role_labels = array();
91 if ( function_exists( 'wp_roles' ) ) {
92 $wp_roles = wp_roles();
93 foreach ( (array) $user->roles as $slug ) {
94 $role_labels[] = isset( $wp_roles->role_names[ $slug ] )
95 ? translate_user_role( $wp_roles->role_names[ $slug ] )
96 : $slug;
97 }
98 }
99 $profile['roleLabels'] = $role_labels;
100 }
101
102 // ----- Counts ------------------------------------------------------
103 // Posts (the post type) by status.
104 $post_status_rows = $wpdb->get_results(
105 $wpdb->prepare(
106 "SELECT post_status, COUNT(*) AS n
107 FROM {$wpdb->posts}
108 WHERE post_author = %d
109 AND post_type = 'post'
110 AND post_status NOT IN ( 'auto-draft', 'inherit', 'trash' )
111 GROUP BY post_status",
112 $user_id
113 ),
114 ARRAY_A
115 );
116 $post_counts = array(
117 'publish' => 0,
118 'draft' => 0,
119 'pending' => 0,
120 'private' => 0,
121 'future' => 0,
122 'total' => 0,
123 );
124 foreach ( (array) $post_status_rows as $row ) {
125 $status = (string) $row['post_status'];
126 $n = (int) $row['n'];
127 $post_counts['total'] += $n;
128 if ( isset( $post_counts[ $status ] ) ) {
129 $post_counts[ $status ] = $n;
130 }
131 }
132
133 // Pages by status.
134 $page_status_rows = $wpdb->get_results(
135 $wpdb->prepare(
136 "SELECT post_status, COUNT(*) AS n
137 FROM {$wpdb->posts}
138 WHERE post_author = %d
139 AND post_type = 'page'
140 AND post_status NOT IN ( 'auto-draft', 'inherit', 'trash' )
141 GROUP BY post_status",
142 $user_id
143 ),
144 ARRAY_A
145 );
146 $page_counts = array(
147 'publish' => 0,
148 'draft' => 0,
149 'total' => 0,
150 );
151 foreach ( (array) $page_status_rows as $row ) {
152 $status = (string) $row['post_status'];
153 $n = (int) $row['n'];
154 $page_counts['total'] += $n;
155 if ( isset( $page_counts[ $status ] ) ) {
156 $page_counts[ $status ] = $n;
157 }
158 }
159
160 // Comments received on posts authored by this user, approved only.
161 $comments_received = (int) $wpdb->get_var(
162 $wpdb->prepare(
163 "SELECT COUNT(c.comment_ID)
164 FROM {$wpdb->comments} c
165 INNER JOIN {$wpdb->posts} p ON c.comment_post_ID = p.ID
166 WHERE p.post_author = %d
167 AND c.comment_approved = '1'
168 AND p.post_status NOT IN ( 'auto-draft', 'trash' )",
169 $user_id
170 )
171 );
172
173 // Comments left BY this user (regardless of post author).
174 $comments_left = (int) $wpdb->get_var(
175 $wpdb->prepare(
176 "SELECT COUNT(*)
177 FROM {$wpdb->comments}
178 WHERE user_id = %d
179 AND comment_approved = '1'",
180 $user_id
181 )
182 );
183
184 // Total content (posts + pages + any custom public post types).
185 $cpt_count = (int) $wpdb->get_var(
186 $wpdb->prepare(
187 "SELECT COUNT(*)
188 FROM {$wpdb->posts}
189 WHERE post_author = %d
190 AND post_type NOT IN ( 'post', 'page', 'attachment', 'revision', 'nav_menu_item' )
191 AND post_status NOT IN ( 'auto-draft', 'inherit', 'trash' )",
192 $user_id
193 )
194 );
195
196 $counts = array(
197 'posts' => $post_counts,
198 'pages' => $page_counts,
199 'commentsReceived' => $comments_received,
200 'commentsLeft' => $comments_left,
201 'cpt' => $cpt_count,
202 );
203
204 // ----- Recent posts (latest 5, any public-ish status) --------------
205 $recent_posts = get_posts(
206 array(
207 'author' => $user_id,
208 'post_type' => array( 'post', 'page' ),
209 'post_status' => array( 'publish', 'private', 'future', 'draft', 'pending' ),
210 'posts_per_page' => 5,
211 'orderby' => 'date',
212 'order' => 'DESC',
213 'suppress_filters' => false,
214 )
215 );
216 $recent = array();
217 foreach ( (array) $recent_posts as $p ) {
218 if ( ! ( $p instanceof WP_Post ) ) {
219 continue;
220 }
221 $recent[] = array(
222 'id' => (int) $p->ID,
223 'title' => get_the_title( $p ),
224 'date' => mysql2date( 'c', $p->post_date_gmt, false ),
225 'status' => (string) $p->post_status,
226 'type' => (string) $p->post_type,
227 'link' => get_permalink( $p ),
228 );
229 }
230
231 // ----- Top categories (most used by this author) -------------------
232 $top_term_rows = $wpdb->get_results(
233 $wpdb->prepare(
234 "SELECT t.term_id, t.name, t.slug, tt.taxonomy, COUNT(*) AS n
235 FROM {$wpdb->term_relationships} tr
236 INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
237 INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
238 INNER JOIN {$wpdb->posts} p ON tr.object_id = p.ID
239 WHERE p.post_author = %d
240 AND p.post_type = 'post'
241 AND p.post_status = 'publish'
242 AND tt.taxonomy IN ( 'category', 'post_tag' )
243 GROUP BY t.term_id, tt.taxonomy
244 ORDER BY n DESC
245 LIMIT 5",
246 $user_id
247 ),
248 ARRAY_A
249 );
250 $top_terms = array();
251 foreach ( (array) $top_term_rows as $row ) {
252 $top_terms[] = array(
253 'id' => (int) $row['term_id'],
254 'name' => (string) $row['name'],
255 'slug' => (string) $row['slug'],
256 'taxonomy' => (string) $row['taxonomy'],
257 'count' => (int) $row['n'],
258 );
259 }
260
261 // ----- Activity (posts published per month, last 12 months) --------
262 // Lightweight sparkline source. Exclude trash + auto-draft, group by
263 // year-month so the JS can fill gaps.
264 $activity_rows = $wpdb->get_results(
265 $wpdb->prepare(
266 "SELECT DATE_FORMAT( post_date_gmt, '%%Y-%%m' ) AS ym, COUNT(*) AS n
267 FROM {$wpdb->posts}
268 WHERE post_author = %d
269 AND post_type IN ( 'post', 'page' )
270 AND post_status = 'publish'
271 AND post_date_gmt >= DATE_SUB( NOW(), INTERVAL 12 MONTH )
272 GROUP BY ym
273 ORDER BY ym ASC",
274 $user_id
275 ),
276 ARRAY_A
277 );
278 $activity = array();
279 foreach ( (array) $activity_rows as $row ) {
280 $activity[] = array(
281 'ym' => (string) $row['ym'],
282 'count' => (int) $row['n'],
283 );
284 }
285
286 // ----- First & last published, plus longest streak -----------------
287 $first_post = $wpdb->get_var(
288 $wpdb->prepare(
289 "SELECT MIN(post_date_gmt) FROM {$wpdb->posts}
290 WHERE post_author = %d AND post_type IN ( 'post', 'page' ) AND post_status = 'publish'",
291 $user_id
292 )
293 );
294 $last_post = $wpdb->get_var(
295 $wpdb->prepare(
296 "SELECT MAX(post_date_gmt) FROM {$wpdb->posts}
297 WHERE post_author = %d AND post_type IN ( 'post', 'page' ) AND post_status = 'publish'",
298 $user_id
299 )
300 );
301 $milestones = array(
302 'firstPublished' => $first_post ? mysql2date( 'c', $first_post, false ) : null,
303 'lastPublished' => $last_post ? mysql2date( 'c', $last_post, false ) : null,
304 );
305
306 $payload = array(
307 'profile' => $profile,
308 'counts' => $counts,
309 'recent' => $recent,
310 'topTerms' => $top_terms,
311 'activity' => $activity,
312 'milestones' => $milestones,
313 );
314
315 /**
316 * Filter the per-user stats payload before it's returned to
317 * the My WordPress folder window. Plugins can drop additional
318 * stat sections (badges, milestones, contribution streaks)
319 * here without forking the JS render.
320 *
321 * @since 0.8.0
322 *
323 * @param array $payload Stats payload.
324 * @param int $user_id Subject user id.
325 */
326 return apply_filters( 'desktop_mode_my_wordpress_user_stats', $payload, $user_id );
327 }
328