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 / user-edit / parts / insights.php

insights.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.10, at apps/user-edit/parts/insights.php

339 lines 11.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * User Edit app — the insights payload behind the profile sidebar
4 * and activity feed: `GET /desktop-mode/v1/users/<id>/insights`.
5 *
6 * - profileCompleteness: filled vs total core fields, percent
7 * - stats: posts / pages / attachments / comments authored /
8 * approved comments received / days since registration / last login
9 * - contentByMonth: last 12 months of posts authored
10 * - recentPosts, recentComments: the last 5 of each (comments BY the
11 * user, not ON their posts)
12 * - sessions: the active session tokens, `current` flagged when known
13 * - applicationPasswords: count + most-recently-used summary
14 *
15 * Each user's payload is computed at most once per minute (a transient
16 * keyed by user id); `?fresh=1` bypasses the cache.
17 *
18 * @package OpenStation
19 */
20
21 defined( 'ABSPATH' ) || exit;
22
23 /**
24 * Register the route.
25 */
26 function openstation_user_edit_window_register_rest_routes() {
27 register_rest_route(
28 'desktop-mode/v1',
29 '/users/(?P<id>\d+)/insights',
30 array(
31 'methods' => WP_REST_Server::READABLE,
32 'callback' => 'openstation_user_edit_window_rest_insights',
33 'permission_callback' => 'openstation_user_edit_window_rest_permission',
34 'args' => array(
35 'id' => array(
36 'required' => true,
37 'type' => 'integer',
38 ),
39 'fresh' => array(
40 'type' => 'boolean',
41 ),
42 ),
43 )
44 );
45 }
46 add_action( 'rest_api_init', 'openstation_user_edit_window_register_rest_routes' );
47
48 /**
49 * `GET /users/<id>/insights` callback.
50 *
51 * @param WP_REST_Request $req Request.
52 * @return WP_REST_Response|WP_Error
53 */
54 function openstation_user_edit_window_rest_insights( $req ) {
55 $id = (int) $req->get_param( 'id' );
56 $user = $id > 0 ? get_userdata( $id ) : null;
57 if ( ! $user instanceof WP_User ) {
58 return new WP_Error( 'openstation_users_not_found', __( 'User not found.', 'desktop-mode' ), array( 'status' => 404 ) );
59 }
60
61 $fresh = (bool) $req->get_param( 'fresh' );
62 $cache_key = 'dm_user_insights_' . $id;
63 $payload = null;
64 if ( ! $fresh ) {
65 $cached = get_transient( $cache_key );
66 if ( is_array( $cached ) ) {
67 $payload = $cached;
68 }
69 }
70 if ( null === $payload ) {
71 $payload = openstation_user_edit_window_compute_insights( $user );
72 }
73
74 // Self-view: the viewer is logged in right now, so a "Never" last
75 // login (an account that predates the tracker) contradicts the
76 // obvious. Pin to now and backfill the meta so future reads no
77 // longer depend on this override — on cache hits as well as fresh
78 // computes, since a cached payload may carry the stale null.
79 if ( (int) get_current_user_id() === $id ) {
80 $now = time();
81 $stored = (int) get_user_meta( $id, OPENSTATION_LAST_LOGIN_META_KEY, true );
82 if ( $stored <= 0 ) {
83 update_user_meta( $id, OPENSTATION_LAST_LOGIN_META_KEY, $now );
84 $stored = $now;
85 }
86 if ( ! isset( $payload['stats'] ) || ! is_array( $payload['stats'] ) ) {
87 $payload['stats'] = array();
88 }
89 if ( empty( $payload['stats']['lastLoginAt'] ) ) {
90 $payload['stats']['lastLoginAt'] = $stored;
91 $payload['stats']['daysSinceLastLogin'] = max( 0, (int) floor( ( $now - $stored ) / DAY_IN_SECONDS ) );
92 }
93 }
94
95 /**
96 * Filter the insights payload before it's returned and cached.
97 *
98 * Plugins can append their own metrics (security-event counts,
99 * subscription tier, last-orders-placed, …) by extending the
100 * `stats` map or adding new top-level keys; the profile tolerates
101 * unknown keys.
102 *
103 * @param array $payload Insights payload.
104 * @param WP_User $user Target user.
105 */
106 $payload = (array) apply_filters( 'openstation_user_edit_window_insights', $payload, $user );
107
108 set_transient( $cache_key, $payload, MINUTE_IN_SECONDS );
109
110 return rest_ensure_response( $payload );
111 }
112
113 /**
114 * The last 12 months of published posts, bucketed by `Y-m`.
115 *
116 * @param int $id User id.
117 * @return array<int,array{month:string,count:int}>
118 */
119 function openstation_user_edit_window_content_by_month( $id ) {
120 global $wpdb;
121 $buckets = array();
122 $now = time();
123 for ( $i = 11; $i >= 0; $i-- ) {
124 $buckets[ gmdate( 'Y-m', strtotime( "-{$i} months", $now ) ) ] = 0;
125 }
126 $rows = (array) $wpdb->get_results(
127 $wpdb->prepare(
128 "SELECT DATE_FORMAT( post_date_gmt, '%%Y-%%m' ) AS bucket, COUNT(*) AS cnt
129 FROM {$wpdb->posts}
130 WHERE post_author = %d
131 AND post_status IN ( 'publish', 'private', 'future' )
132 AND post_date_gmt >= %s
133 GROUP BY bucket
134 ORDER BY bucket ASC",
135 $id,
136 gmdate( 'Y-m-01 00:00:00', strtotime( '-12 months', $now ) )
137 ),
138 ARRAY_A
139 );
140 foreach ( $rows as $row ) {
141 $bucket = isset( $row['bucket'] ) ? (string) $row['bucket'] : '';
142 if ( isset( $buckets[ $bucket ] ) ) {
143 $buckets[ $bucket ] = (int) $row['cnt'];
144 }
145 }
146 $out = array();
147 foreach ( $buckets as $bucket => $cnt ) {
148 $out[] = array(
149 'month' => $bucket,
150 'count' => $cnt,
151 );
152 }
153 return $out;
154 }
155
156 /**
157 * A GMT datetime, or the local one converted, for rows whose GMT
158 * column is the zero date (a draft never published).
159 *
160 * @param string $gmt The GMT column.
161 * @param string $local The local column.
162 * @return string
163 */
164 function openstation_user_edit_window_gmt_or_local( $gmt, $local ) {
165 $gmt = (string) $gmt;
166 if ( '' === $gmt || 0 === strpos( $gmt, '0000-00-00' ) ) {
167 return (string) get_gmt_from_date( (string) $local );
168 }
169 return $gmt;
170 }
171
172 /**
173 * The active sessions of a user, the current device flagged.
174 *
175 * @param int $id User id.
176 * @return array<int,array<string,mixed>>
177 */
178 function openstation_user_edit_window_sessions( $id ) {
179 if ( ! class_exists( 'WP_Session_Tokens' ) ) {
180 return array();
181 }
182 // The meta blob's keys are verifiers — hashes of the raw cookie
183 // token — so hash the current token the same way before comparing.
184 $current_token = wp_get_session_token();
185 $current_verifier = $current_token ? hash( 'sha256', $current_token ) : '';
186 $sessions = array();
187 $now = time();
188 foreach ( (array) get_user_meta( $id, 'session_tokens', true ) as $hash => $info ) {
189 if ( ! is_array( $info ) ) {
190 continue;
191 }
192 $expires = isset( $info['expiration'] ) ? (int) $info['expiration'] : 0;
193 if ( $expires > 0 && $expires < $now ) {
194 continue;
195 }
196 $sessions[] = array(
197 'expiration' => $expires,
198 'login' => isset( $info['login'] ) ? (int) $info['login'] : 0,
199 'ip' => isset( $info['ip'] ) ? (string) $info['ip'] : '',
200 'ua' => isset( $info['ua'] ) ? (string) $info['ua'] : '',
201 'current' => '' !== $current_verifier && $current_verifier === $hash,
202 );
203 }
204 return $sessions;
205 }
206
207 /**
208 * Compute the insights payload for a user. Public so a plugin can call
209 * it from its own route without the HTTP cycle.
210 *
211 * @param WP_User $user Target user.
212 * @return array<string,mixed>
213 */
214 function openstation_user_edit_window_compute_insights( WP_User $user ) {
215 global $wpdb;
216 $id = (int) $user->ID;
217
218 $completeness_fields = array( $user->first_name, $user->last_name, $user->nickname, $user->description, $user->user_url, $user->user_email );
219 $filled = count( array_filter( array_map( 'trim', array_map( 'strval', $completeness_fields ) ) ) );
220 $total = count( $completeness_fields );
221
222 $received_comments = (int) $wpdb->get_var(
223 $wpdb->prepare(
224 "SELECT COUNT(c.comment_ID) FROM {$wpdb->comments} c
225 INNER JOIN {$wpdb->posts} p ON p.ID = c.comment_post_ID
226 WHERE p.post_author = %d AND p.post_status = 'publish' AND c.comment_approved = '1'",
227 $id
228 )
229 );
230
231 $recent_posts = array();
232 foreach (
233 get_posts(
234 array(
235 'author' => $id,
236 'post_type' => 'any',
237 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ),
238 'posts_per_page' => 5,
239 'orderby' => 'date',
240 'order' => 'DESC',
241 )
242 ) as $post
243 ) {
244 $recent_posts[] = array(
245 'id' => (int) $post->ID,
246 'title' => '' !== $post->post_title ? $post->post_title : __( '(no title)', 'desktop-mode' ),
247 'status' => (string) $post->post_status,
248 'type' => (string) $post->post_type,
249 'dateGmt' => openstation_user_edit_window_gmt_or_local( $post->post_date_gmt, $post->post_date ),
250 'commentCount' => (int) $post->comment_count,
251 'permalink' => (string) get_permalink( $post ),
252 'editUrl' => (string) get_edit_post_link( $post->ID, 'raw' ),
253 );
254 }
255
256 $recent_comments = array();
257 foreach (
258 (array) get_comments(
259 array(
260 'user_id' => $id,
261 'number' => 5,
262 'orderby' => 'comment_date_gmt',
263 'order' => 'DESC',
264 )
265 ) as $comment
266 ) {
267 $post = $comment->comment_post_ID ? get_post( (int) $comment->comment_post_ID ) : null;
268 $recent_comments[] = array(
269 'id' => (int) $comment->comment_ID,
270 'postId' => (int) $comment->comment_post_ID,
271 'postTitle' => $post instanceof WP_Post ? ( '' !== $post->post_title ? $post->post_title : __( '(no title)', 'desktop-mode' ) ) : '',
272 'excerpt' => wp_trim_words( wp_strip_all_tags( (string) $comment->comment_content ), 24 ),
273 'dateGmt' => openstation_user_edit_window_gmt_or_local( $comment->comment_date_gmt, $comment->comment_date ),
274 'approved' => '1' === (string) $comment->comment_approved,
275 );
276 }
277
278 $app_passwords = array(
279 'total' => 0,
280 'lastUsedAt' => null,
281 'lastUsedName' => null,
282 );
283 if ( class_exists( 'WP_Application_Passwords' ) ) {
284 $apps = (array) WP_Application_Passwords::get_user_application_passwords( $id );
285 $app_passwords['total'] = count( $apps );
286 $best = 0;
287 foreach ( $apps as $app ) {
288 $used = isset( $app['last_used'] ) ? (int) $app['last_used'] : 0;
289 if ( $used > $best ) {
290 $best = $used;
291 $app_passwords['lastUsedAt'] = $used;
292 $app_passwords['lastUsedName'] = isset( $app['name'] ) ? (string) $app['name'] : null;
293 }
294 }
295 }
296
297 $registered_ts = strtotime( (string) $user->user_registered . ' UTC' );
298 $last_login_ts = (int) get_user_meta( $id, OPENSTATION_LAST_LOGIN_META_KEY, true );
299 $last_login_ts = $last_login_ts > 0 ? $last_login_ts : null;
300 $days = static function ( $since ) {
301 return $since ? max( 0, (int) floor( ( time() - $since ) / DAY_IN_SECONDS ) ) : null;
302 };
303
304 return array(
305 'userId' => $id,
306 'displayName' => (string) $user->display_name,
307 'avatarUrl' => (string) get_avatar_url( $id, array( 'size' => 96 ) ),
308 'profileUrl' => (string) get_author_posts_url( $id ),
309 'roles' => array_values( (array) $user->roles ),
310 'capabilitiesCount' => is_array( $user->allcaps ) ? count( array_filter( $user->allcaps ) ) : 0,
311 'profileCompleteness' => array(
312 'filled' => $filled,
313 'total' => $total,
314 'percent' => $total > 0 ? (int) round( ( $filled / $total ) * 100 ) : 0,
315 ),
316 'stats' => array(
317 'posts' => (int) count_user_posts( $id, 'post', true ),
318 'pages' => post_type_exists( 'page' ) ? (int) count_user_posts( $id, 'page', true ) : 0,
319 'attachments' => (int) count_user_posts( $id, 'attachment', true ),
320 'commentsAuthored' => (int) get_comments(
321 array(
322 'user_id' => $id,
323 'count' => true,
324 )
325 ),
326 'commentsReceived' => $received_comments,
327 'daysSinceRegistration' => $days( $registered_ts ),
328 'lastLoginAt' => $last_login_ts,
329 'daysSinceLastLogin' => $days( $last_login_ts ),
330 'registeredAt' => $registered_ts ? $registered_ts : null,
331 ),
332 'contentByMonth' => openstation_user_edit_window_content_by_month( $id ),
333 'recentPosts' => $recent_posts,
334 'recentComments' => $recent_comments,
335 'sessions' => openstation_user_edit_window_sessions( $id ),
336 'applicationPasswords' => $app_passwords,
337 );
338 }
339