| 1 |
<?php |
| 2 |
/** |
| 3 |
* OpenStation — My WordPress: per-user activity footprint endpoint. |
| 4 |
* |
| 5 |
* `GET /desktop-mode/v1/user-footprint/<id>` returns a deep activity |
| 6 |
* footprint for one user: a year of day-by-day publishing counts |
| 7 |
* (GitHub-style calendar heatmap), weekday and hour-of-day |
| 8 |
* distribution (publishing rhythm), longest publishing streak, and |
| 9 |
* a recent-events timeline (posts published + comments left, last |
| 10 |
* 30). The right-click "View activity footprint" action in the My |
| 11 |
* WordPress users folder paints from this single payload. |
| 12 |
* |
| 13 |
* Permission: any logged-in user (the dossier route already has |
| 14 |
* the same gate). Sensitive fields (email, IP) are NOT returned |
| 15 |
* from this endpoint — `user-stats.php` carries those for the |
| 16 |
* preview pane, and the footprint focuses on activity patterns. |
| 17 |
* Timeline rows whose underlying post is not published are only |
| 18 |
* emitted when the viewer passes `current_user_can( 'read_post' )` |
| 19 |
* for that post, so draft/pending/private/future titles never leak |
| 20 |
* to ordinary logged-in users. |
| 21 |
* |
| 22 |
* Payload shape: |
| 23 |
* |
| 24 |
* { |
| 25 |
* profile: { id, name, avatarUrl, link, roleLabels?, registered? }, |
| 26 |
* range: { from, to, days }, // YYYY-MM-DD bookends + day count |
| 27 |
* daily: [ { date, posts, comments, updates } ], // length = range.days; missing days = 0 |
| 28 |
* weekday: [ 0..6 ], // post counts, Sunday-indexed |
| 29 |
* hour: [ 0..23 ], // post counts, server-local hour |
| 30 |
* streak: { longest, current, longestRange:{ from, to } }, |
| 31 |
* timeline:[ // 30 most recent activity rows |
| 32 |
* { kind:'post'|'comment'|'post-update', date, title, link, status, postId?, type? } |
| 33 |
* ], |
| 34 |
* totals: { posts, pages, comments, updates, mostProlificMonth?:{ ym, n } } |
| 35 |
* } |
| 36 |
* |
| 37 |
* Timeline row fields: |
| 38 |
* - `kind` — discriminator: `'post'` (publish), `'comment'`, or |
| 39 |
* `'post-update'` (revision rollup). |
| 40 |
* - `type` — only set when `kind` is `'post'` or `'post-update'`. |
| 41 |
* Carries the post's CPT slug (`'post'`, `'page'`, custom |
| 42 |
* types) so the renderer can pick a Post-vs-Page icon |
| 43 |
* without a second REST lookup. |
| 44 |
* |
| 45 |
* "Updates" are revisions saved by the user AFTER a post's original |
| 46 |
* creation — i.e. the user opened an existing post and saved it |
| 47 |
* again. The initial save (which WordPress also writes as a revision) |
| 48 |
* is excluded so the per-day "updates" count doesn't double up with |
| 49 |
* the per-day "posts" count. |
| 50 |
* |
| 51 |
* @package OpenStation |
| 52 |
*/ |
| 53 |
|
| 54 |
defined( 'ABSPATH' ) || exit; |
| 55 |
|
| 56 |
/** |
| 57 |
* Register the route. |
| 58 |
*/ |
| 59 |
function openstation_my_wordpress_register_user_footprint_route() { |
| 60 |
register_rest_route( |
| 61 |
'desktop-mode/v1', |
| 62 |
'/user-footprint/(?P<id>\d+)', |
| 63 |
array( |
| 64 |
'methods' => WP_REST_Server::READABLE, |
| 65 |
'callback' => 'openstation_my_wordpress_user_footprint_callback', |
| 66 |
'permission_callback' => static function () { |
| 67 |
return is_user_logged_in(); |
| 68 |
}, |
| 69 |
'args' => array( |
| 70 |
'id' => array( |
| 71 |
'required' => true, |
| 72 |
'type' => 'integer', |
| 73 |
'sanitize_callback' => 'absint', |
| 74 |
), |
| 75 |
), |
| 76 |
) |
| 77 |
); |
| 78 |
} |
| 79 |
add_action( 'rest_api_init', 'openstation_my_wordpress_register_user_footprint_route' ); |
| 80 |
|
| 81 |
/** |
| 82 |
* Aggregator callback. See the file docblock for the payload shape. |
| 83 |
* |
| 84 |
* @param WP_REST_Request $request REST request. |
| 85 |
* @return array|WP_Error |
| 86 |
*/ |
| 87 |
function openstation_my_wordpress_user_footprint_callback( $request ) { |
| 88 |
global $wpdb; |
| 89 |
|
| 90 |
$user_id = (int) $request->get_param( 'id' ); |
| 91 |
$user = get_userdata( $user_id ); |
| 92 |
if ( ! $user ) { |
| 93 |
return new WP_Error( |
| 94 |
'openstation_user_not_found', |
| 95 |
__( 'User not found.', 'desktop-mode' ), |
| 96 |
array( 'status' => 404 ) |
| 97 |
); |
| 98 |
} |
| 99 |
|
| 100 |
$can_see_private = current_user_can( 'list_users' ) |
| 101 |
|| ( get_current_user_id() === $user_id ); |
| 102 |
|
| 103 |
// ---- Profile (minimal — the dossier already returned the full one) ---- |
| 104 |
$profile = array( |
| 105 |
'id' => (int) $user->ID, |
| 106 |
'name' => (string) $user->display_name, |
| 107 |
'avatarUrl' => get_avatar_url( $user->ID, array( 'size' => 128 ) ), |
| 108 |
'link' => get_author_posts_url( $user->ID ), |
| 109 |
); |
| 110 |
if ( $can_see_private ) { |
| 111 |
$role_labels = array(); |
| 112 |
if ( function_exists( 'wp_roles' ) ) { |
| 113 |
$wp_roles = wp_roles(); |
| 114 |
foreach ( (array) $user->roles as $slug ) { |
| 115 |
$role_labels[] = isset( $wp_roles->role_names[ $slug ] ) |
| 116 |
? translate_user_role( $wp_roles->role_names[ $slug ] ) |
| 117 |
: $slug; |
| 118 |
} |
| 119 |
} |
| 120 |
$profile['roleLabels'] = $role_labels; |
| 121 |
if ( '' !== $user->user_registered ) { |
| 122 |
$profile['registered'] = mysql2date( 'c', $user->user_registered, false ); |
| 123 |
} |
| 124 |
} |
| 125 |
|
| 126 |
// ---- Range: rolling 365-day window ending today (UTC bookends) ------- |
| 127 |
$days = 365; |
| 128 |
$now = time(); // UTC |
| 129 |
$from_ts = strtotime( '-' . ( $days - 1 ) . ' days', $now ); |
| 130 |
$to_ts = $now; |
| 131 |
$range = array( |
| 132 |
'from' => gmdate( 'Y-m-d', $from_ts ), |
| 133 |
'to' => gmdate( 'Y-m-d', $to_ts ), |
| 134 |
'days' => $days, |
| 135 |
); |
| 136 |
|
| 137 |
// ---- Daily counts (posts published per day + comments LEFT per day) -- |
| 138 |
// Two queries (one for posts, one for comments), each grouped by |
| 139 |
// `DATE(post_date_gmt)` / `DATE(comment_date_gmt)`. Then we |
| 140 |
// densify to a full day-by-day array so the heatmap renders |
| 141 |
// every cell, even empty ones. |
| 142 |
$post_rows = $wpdb->get_results( |
| 143 |
$wpdb->prepare( |
| 144 |
"SELECT DATE(post_date_gmt) AS d, COUNT(*) AS n |
| 145 |
FROM {$wpdb->posts} |
| 146 |
WHERE post_author = %d |
| 147 |
AND post_status = 'publish' |
| 148 |
AND post_type IN ( 'post', 'page' ) |
| 149 |
AND post_date_gmt >= %s |
| 150 |
GROUP BY d |
| 151 |
ORDER BY d ASC", |
| 152 |
$user_id, |
| 153 |
gmdate( 'Y-m-d 00:00:00', $from_ts ) |
| 154 |
), |
| 155 |
ARRAY_A |
| 156 |
); |
| 157 |
$post_by_day = array(); |
| 158 |
foreach ( (array) $post_rows as $row ) { |
| 159 |
$post_by_day[ (string) $row['d'] ] = (int) $row['n']; |
| 160 |
} |
| 161 |
|
| 162 |
$comment_rows = $wpdb->get_results( |
| 163 |
$wpdb->prepare( |
| 164 |
"SELECT DATE(comment_date_gmt) AS d, COUNT(*) AS n |
| 165 |
FROM {$wpdb->comments} |
| 166 |
WHERE user_id = %d |
| 167 |
AND comment_approved = '1' |
| 168 |
AND comment_date_gmt >= %s |
| 169 |
GROUP BY d |
| 170 |
ORDER BY d ASC", |
| 171 |
$user_id, |
| 172 |
gmdate( 'Y-m-d 00:00:00', $from_ts ) |
| 173 |
), |
| 174 |
ARRAY_A |
| 175 |
); |
| 176 |
$comment_by_day = array(); |
| 177 |
foreach ( (array) $comment_rows as $row ) { |
| 178 |
$comment_by_day[ (string) $row['d'] ] = (int) $row['n']; |
| 179 |
} |
| 180 |
|
| 181 |
// Updates = revisions saved by this user, joined back to the |
| 182 |
// parent post so we can skip the initial-save revision (where the |
| 183 |
// revision's `post_date_gmt` equals the parent's `post_date_gmt`). |
| 184 |
// `r.post_author` (not the parent's) tracks who hit Save, so |
| 185 |
// updates an editor makes to someone else's post show up on the |
| 186 |
// editor's footprint — same shape GitHub's contribution graph |
| 187 |
// uses for commits across repos you don't own. |
| 188 |
$update_rows = $wpdb->get_results( |
| 189 |
$wpdb->prepare( |
| 190 |
"SELECT DATE(r.post_date_gmt) AS d, COUNT(*) AS n |
| 191 |
FROM {$wpdb->posts} r |
| 192 |
INNER JOIN {$wpdb->posts} p ON r.post_parent = p.ID |
| 193 |
WHERE r.post_author = %d |
| 194 |
AND r.post_type = 'revision' |
| 195 |
AND r.post_status = 'inherit' |
| 196 |
AND r.post_date_gmt > p.post_date_gmt |
| 197 |
AND r.post_date_gmt >= %s |
| 198 |
GROUP BY d |
| 199 |
ORDER BY d ASC", |
| 200 |
$user_id, |
| 201 |
gmdate( 'Y-m-d 00:00:00', $from_ts ) |
| 202 |
), |
| 203 |
ARRAY_A |
| 204 |
); |
| 205 |
$update_by_day = array(); |
| 206 |
foreach ( (array) $update_rows as $row ) { |
| 207 |
$update_by_day[ (string) $row['d'] ] = (int) $row['n']; |
| 208 |
} |
| 209 |
|
| 210 |
$daily = array(); |
| 211 |
for ( $i = 0; $i < $days; ++$i ) { |
| 212 |
$ts = strtotime( '+' . $i . ' days', $from_ts ); |
| 213 |
$date = gmdate( 'Y-m-d', $ts ); |
| 214 |
$daily[] = array( |
| 215 |
'date' => $date, |
| 216 |
'posts' => isset( $post_by_day[ $date ] ) ? $post_by_day[ $date ] : 0, |
| 217 |
'comments' => isset( $comment_by_day[ $date ] ) ? $comment_by_day[ $date ] : 0, |
| 218 |
'updates' => isset( $update_by_day[ $date ] ) ? $update_by_day[ $date ] : 0, |
| 219 |
); |
| 220 |
} |
| 221 |
|
| 222 |
// ---- Weekday distribution (Sunday-indexed) --------------------------- |
| 223 |
// `DAYOFWEEK` returns 1=Sunday through 7=Saturday in MySQL. |
| 224 |
$weekday_rows = $wpdb->get_results( |
| 225 |
$wpdb->prepare( |
| 226 |
"SELECT DAYOFWEEK(post_date_gmt) AS dow, COUNT(*) AS n |
| 227 |
FROM {$wpdb->posts} |
| 228 |
WHERE post_author = %d |
| 229 |
AND post_status = 'publish' |
| 230 |
AND post_type IN ( 'post', 'page' ) |
| 231 |
GROUP BY dow", |
| 232 |
$user_id |
| 233 |
), |
| 234 |
ARRAY_A |
| 235 |
); |
| 236 |
$weekday = array( 0, 0, 0, 0, 0, 0, 0 ); |
| 237 |
foreach ( (array) $weekday_rows as $row ) { |
| 238 |
$dow = (int) $row['dow']; |
| 239 |
if ( $dow >= 1 && $dow <= 7 ) { |
| 240 |
$weekday[ $dow - 1 ] = (int) $row['n']; |
| 241 |
} |
| 242 |
} |
| 243 |
|
| 244 |
// ---- Hour-of-day distribution (0..23, site timezone) ----------------- |
| 245 |
// `post_date` is already in site timezone — that's the timestamp |
| 246 |
// the author saw when they hit Publish. Using GMT here would shift |
| 247 |
// the bars by the offset and feel wrong to anyone in a non-UTC tz. |
| 248 |
$hour_rows = $wpdb->get_results( |
| 249 |
$wpdb->prepare( |
| 250 |
"SELECT HOUR(post_date) AS h, COUNT(*) AS n |
| 251 |
FROM {$wpdb->posts} |
| 252 |
WHERE post_author = %d |
| 253 |
AND post_status = 'publish' |
| 254 |
AND post_type IN ( 'post', 'page' ) |
| 255 |
GROUP BY h", |
| 256 |
$user_id |
| 257 |
), |
| 258 |
ARRAY_A |
| 259 |
); |
| 260 |
$hour = array_fill( 0, 24, 0 ); |
| 261 |
foreach ( (array) $hour_rows as $row ) { |
| 262 |
$h = (int) $row['h']; |
| 263 |
if ( $h >= 0 && $h <= 23 ) { |
| 264 |
$hour[ $h ] = (int) $row['n']; |
| 265 |
} |
| 266 |
} |
| 267 |
|
| 268 |
// ---- Streak (longest consecutive run of days with ≥1 post over the |
| 269 |
// 365-day window; current run ending today). ------------------------ |
| 270 |
$longest = 0; |
| 271 |
$current = 0; |
| 272 |
$longest_run = 0; |
| 273 |
$longest_from = ''; |
| 274 |
$longest_to = ''; |
| 275 |
$run_start = ''; |
| 276 |
$today_str = $range['to']; |
| 277 |
$prev_day_active = false; |
| 278 |
|
| 279 |
// "Active" = published a post, left a comment, or saved a revision. |
| 280 |
// Pre-0.8.7 this only counted publish days, so an editor doing |
| 281 |
// daily updates without new posts had a "0 day" streak — wrong |
| 282 |
// flavour of GitHub-style for a CMS where most work is editing. |
| 283 |
$is_active = static function ( $entry ) { |
| 284 |
return $entry['posts'] > 0 |
| 285 |
|| ( isset( $entry['updates'] ) && $entry['updates'] > 0 ) |
| 286 |
|| ( isset( $entry['comments'] ) && $entry['comments'] > 0 ); |
| 287 |
}; |
| 288 |
foreach ( $daily as $entry ) { |
| 289 |
if ( $is_active( $entry ) ) { |
| 290 |
if ( ! $prev_day_active ) { |
| 291 |
$run_start = $entry['date']; |
| 292 |
} |
| 293 |
++$longest_run; |
| 294 |
if ( $longest_run > $longest ) { |
| 295 |
$longest = $longest_run; |
| 296 |
$longest_from = $run_start; |
| 297 |
$longest_to = $entry['date']; |
| 298 |
} |
| 299 |
$prev_day_active = true; |
| 300 |
} else { |
| 301 |
$longest_run = 0; |
| 302 |
$prev_day_active = false; |
| 303 |
} |
| 304 |
} |
| 305 |
// Current streak — walk backward from today. |
| 306 |
for ( $i = count( $daily ) - 1; $i >= 0; --$i ) { |
| 307 |
if ( $is_active( $daily[ $i ] ) ) { |
| 308 |
++$current; |
| 309 |
} else { |
| 310 |
break; |
| 311 |
} |
| 312 |
} |
| 313 |
$streak = array( |
| 314 |
'longest' => $longest, |
| 315 |
'current' => $current, |
| 316 |
'longestRange' => array( |
| 317 |
'from' => $longest_from, |
| 318 |
'to' => $longest_to, |
| 319 |
), |
| 320 |
); |
| 321 |
|
| 322 |
// ---- Timeline: 30 most recent posts + comments, interleaved by date - |
| 323 |
// One query per kind, then merge + sort + slice in PHP. Smaller and |
| 324 |
// simpler than a SQL `UNION ALL`, and each branch already has the |
| 325 |
// right index. |
| 326 |
$timeline_posts = $wpdb->get_results( |
| 327 |
$wpdb->prepare( |
| 328 |
"SELECT ID, post_title, post_status, post_date_gmt, post_type |
| 329 |
FROM {$wpdb->posts} |
| 330 |
WHERE post_author = %d |
| 331 |
AND post_type IN ( 'post', 'page' ) |
| 332 |
AND post_status NOT IN ( 'auto-draft', 'inherit', 'trash' ) |
| 333 |
ORDER BY post_date_gmt DESC |
| 334 |
LIMIT 30", |
| 335 |
$user_id |
| 336 |
), |
| 337 |
ARRAY_A |
| 338 |
); |
| 339 |
$timeline_comments = $wpdb->get_results( |
| 340 |
$wpdb->prepare( |
| 341 |
"SELECT c.comment_ID, c.comment_post_ID, c.comment_date_gmt, c.comment_approved, |
| 342 |
p.post_title, p.post_status |
| 343 |
FROM {$wpdb->comments} c |
| 344 |
LEFT JOIN {$wpdb->posts} p ON c.comment_post_ID = p.ID |
| 345 |
WHERE c.user_id = %d |
| 346 |
AND c.comment_approved = '1' |
| 347 |
ORDER BY c.comment_date_gmt DESC |
| 348 |
LIMIT 30", |
| 349 |
$user_id |
| 350 |
), |
| 351 |
ARRAY_A |
| 352 |
); |
| 353 |
// Recent updates — newest revision per parent post saved by this |
| 354 |
// user. We collapse per-parent (`GROUP BY r.post_parent`) so a |
| 355 |
// burst of saves on one post reads as one row in the activity |
| 356 |
// list (otherwise an editor polishing a single article would push |
| 357 |
// every other event off the screen). The MAX(r.post_date_gmt) |
| 358 |
// surfaces the most recent save as the row's timestamp. |
| 359 |
$timeline_updates = $wpdb->get_results( |
| 360 |
$wpdb->prepare( |
| 361 |
"SELECT r.post_parent AS parent_id, MAX(r.post_date_gmt) AS last_save, p.post_title, p.post_status, p.post_type |
| 362 |
FROM {$wpdb->posts} r |
| 363 |
INNER JOIN {$wpdb->posts} p ON r.post_parent = p.ID |
| 364 |
WHERE r.post_author = %d |
| 365 |
AND r.post_type = 'revision' |
| 366 |
AND r.post_status = 'inherit' |
| 367 |
AND r.post_date_gmt > p.post_date_gmt |
| 368 |
AND p.post_status NOT IN ( 'auto-draft', 'inherit', 'trash' ) |
| 369 |
GROUP BY r.post_parent |
| 370 |
ORDER BY last_save DESC |
| 371 |
LIMIT 30", |
| 372 |
$user_id |
| 373 |
), |
| 374 |
ARRAY_A |
| 375 |
); |
| 376 |
$timeline = array(); |
| 377 |
// Per-row gate: rows for non-published posts (draft, pending, |
| 378 |
// private, future, …) carry titles the viewer may not be allowed |
| 379 |
// to see. `read_post` resolves to the right meta cap per status, |
| 380 |
// so authors/editors keep their full timeline while ordinary |
| 381 |
// logged-in users only see published work. |
| 382 |
foreach ( (array) $timeline_posts as $p ) { |
| 383 |
$pid = (int) $p['ID']; |
| 384 |
if ( 'publish' !== (string) $p['post_status'] && ! current_user_can( 'read_post', $pid ) ) { |
| 385 |
continue; |
| 386 |
} |
| 387 |
$timeline[] = array( |
| 388 |
'kind' => 'post', |
| 389 |
'date' => mysql2date( 'c', $p['post_date_gmt'], false ), |
| 390 |
'title' => (string) $p['post_title'], |
| 391 |
'status' => (string) $p['post_status'], |
| 392 |
'postId' => $pid, |
| 393 |
'link' => (string) get_permalink( $pid ), |
| 394 |
'type' => (string) $p['post_type'], |
| 395 |
); |
| 396 |
} |
| 397 |
foreach ( (array) $timeline_comments as $c ) { |
| 398 |
$pid = (int) $c['comment_post_ID']; |
| 399 |
// LEFT-joined parent: a NULL status means the post is gone — |
| 400 |
// nothing to leak, keep the row (title is already ''). A |
| 401 |
// non-published parent leaks its title via the join, so it |
| 402 |
// gets the same `read_post` gate as the post rows above. |
| 403 |
if ( isset( $c['post_status'] ) && 'publish' !== (string) $c['post_status'] && ! current_user_can( 'read_post', $pid ) ) { |
| 404 |
continue; |
| 405 |
} |
| 406 |
$timeline[] = array( |
| 407 |
'kind' => 'comment', |
| 408 |
'date' => mysql2date( 'c', $c['comment_date_gmt'], false ), |
| 409 |
'title' => (string) ( $c['post_title'] ?? '' ), |
| 410 |
'status' => 'approved', |
| 411 |
'postId' => $pid, |
| 412 |
'link' => $pid ? (string) get_permalink( $pid ) : '', |
| 413 |
); |
| 414 |
} |
| 415 |
foreach ( (array) $timeline_updates as $u ) { |
| 416 |
$pid = (int) $u['parent_id']; |
| 417 |
if ( 'publish' !== (string) $u['post_status'] && ! current_user_can( 'read_post', $pid ) ) { |
| 418 |
continue; |
| 419 |
} |
| 420 |
$timeline[] = array( |
| 421 |
'kind' => 'post-update', |
| 422 |
'date' => mysql2date( 'c', $u['last_save'], false ), |
| 423 |
'title' => (string) $u['post_title'], |
| 424 |
'status' => (string) $u['post_status'], |
| 425 |
'postId' => $pid, |
| 426 |
'link' => $pid ? (string) get_permalink( $pid ) : '', |
| 427 |
'type' => (string) $u['post_type'], |
| 428 |
); |
| 429 |
} |
| 430 |
usort( |
| 431 |
$timeline, |
| 432 |
static function ( $a, $b ) { |
| 433 |
return strcmp( (string) $b['date'], (string) $a['date'] ); |
| 434 |
} |
| 435 |
); |
| 436 |
$timeline = array_slice( $timeline, 0, 30 ); |
| 437 |
|
| 438 |
// ---- Totals + most-prolific month ----------------------------------- |
| 439 |
$totals_posts = (int) $wpdb->get_var( |
| 440 |
$wpdb->prepare( |
| 441 |
"SELECT COUNT(*) FROM {$wpdb->posts} |
| 442 |
WHERE post_author = %d |
| 443 |
AND post_type = 'post' |
| 444 |
AND post_status NOT IN ( 'auto-draft', 'inherit', 'trash' )", |
| 445 |
$user_id |
| 446 |
) |
| 447 |
); |
| 448 |
$totals_pages = (int) $wpdb->get_var( |
| 449 |
$wpdb->prepare( |
| 450 |
"SELECT COUNT(*) FROM {$wpdb->posts} |
| 451 |
WHERE post_author = %d |
| 452 |
AND post_type = 'page' |
| 453 |
AND post_status NOT IN ( 'auto-draft', 'inherit', 'trash' )", |
| 454 |
$user_id |
| 455 |
) |
| 456 |
); |
| 457 |
$totals_comments = (int) $wpdb->get_var( |
| 458 |
$wpdb->prepare( |
| 459 |
"SELECT COUNT(*) FROM {$wpdb->comments} |
| 460 |
WHERE user_id = %d AND comment_approved = '1'", |
| 461 |
$user_id |
| 462 |
) |
| 463 |
); |
| 464 |
// Lifetime updates = revisions this user saved after the initial |
| 465 |
// creation of the parent post. Matches the per-day `updates` |
| 466 |
// definition so the hero stat and heatmap rollups agree. |
| 467 |
$totals_updates = (int) $wpdb->get_var( |
| 468 |
$wpdb->prepare( |
| 469 |
"SELECT COUNT(*) FROM {$wpdb->posts} r |
| 470 |
INNER JOIN {$wpdb->posts} p ON r.post_parent = p.ID |
| 471 |
WHERE r.post_author = %d |
| 472 |
AND r.post_type = 'revision' |
| 473 |
AND r.post_status = 'inherit' |
| 474 |
AND r.post_date_gmt > p.post_date_gmt", |
| 475 |
$user_id |
| 476 |
) |
| 477 |
); |
| 478 |
$month_row = $wpdb->get_row( |
| 479 |
$wpdb->prepare( |
| 480 |
"SELECT DATE_FORMAT(post_date_gmt, '%%Y-%%m') AS ym, COUNT(*) AS n |
| 481 |
FROM {$wpdb->posts} |
| 482 |
WHERE post_author = %d |
| 483 |
AND post_status = 'publish' |
| 484 |
AND post_type IN ( 'post', 'page' ) |
| 485 |
GROUP BY ym |
| 486 |
ORDER BY n DESC |
| 487 |
LIMIT 1", |
| 488 |
$user_id |
| 489 |
), |
| 490 |
ARRAY_A |
| 491 |
); |
| 492 |
$totals = array( |
| 493 |
'posts' => $totals_posts, |
| 494 |
'pages' => $totals_pages, |
| 495 |
'comments' => $totals_comments, |
| 496 |
'updates' => $totals_updates, |
| 497 |
); |
| 498 |
if ( $month_row && isset( $month_row['ym'] ) ) { |
| 499 |
$totals['mostProlificMonth'] = array( |
| 500 |
'ym' => (string) $month_row['ym'], |
| 501 |
'n' => (int) $month_row['n'], |
| 502 |
); |
| 503 |
} |
| 504 |
|
| 505 |
$payload = array( |
| 506 |
'profile' => $profile, |
| 507 |
'range' => $range, |
| 508 |
'daily' => $daily, |
| 509 |
'weekday' => $weekday, |
| 510 |
'hour' => $hour, |
| 511 |
'streak' => $streak, |
| 512 |
'timeline' => $timeline, |
| 513 |
'totals' => $totals, |
| 514 |
); |
| 515 |
|
| 516 |
/** |
| 517 |
* Filter the per-user footprint payload before it's returned to |
| 518 |
* the My WordPress folder window. Plugins can extend the timeline |
| 519 |
* with their own activity rows, or replace the streak math with |
| 520 |
* something domain-specific. |
| 521 |
* |
| 522 |
* @param array $payload Footprint payload. |
| 523 |
* @param int $user_id Subject user id. |
| 524 |
*/ |
| 525 |
return apply_filters( 'openstation_my_wordpress_user_footprint', $payload, $user_id ); |
| 526 |
} |
| 527 |
|