| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | - * Desktop Mode — My WordPress: per-user REST fields for the | |
| 3 | + * OpenStation — My WordPress: per-user REST fields for the | |
| 4 | 4 | * Users-folder list view. |
| 5 | 5 | * |
| 6 | 6 | * Surfaces a small "summary" payload on every `/wp/v2/users` row so |
| 7 | 7 | * the My WordPress folder window can paint a rich tile (avatar + |
| @@ -9,12 +9,12 @@ | ||
| 9 | 9 | * N+1 dossier requests as the user scrolls. |
| 10 | 10 | * |
| 11 | 11 | * Fields: |
| 12 | 12 | * |
| 13 | - * - `desktop_mode_summary.postCount` int count of non-trash posts authored | |
| 14 | - * - `desktop_mode_summary.roleLabels` array translated role labels (gated on `list_users` OR self) | |
| 15 | - * - `desktop_mode_summary.registered` string ISO-8601 registered date (gated on `list_users` OR self) | |
| 16 | - * - `desktop_mode_summary.lastActive` string ISO-8601 of latest published post, or '' when unknown | |
| 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 | 17 | * |
| 18 | 18 | * Each field is independently capability-gated so a viewer without |
| 19 | 19 | * `list_users` still sees `postCount` (public counts) and a public |
| 20 | 20 | * `lastActive` derived from publish dates, but doesn't see |
| @@ -19,25 +19,143 @@ | ||
| 19 | 19 | * `list_users` still sees `postCount` (public counts) and a public |
| 20 | 20 | * `lastActive` derived from publish dates, but doesn't see |
| 21 | 21 | * `roleLabels` or `registered` (private). |
| 22 | 22 | * |
| 23 | - * @package WPDesktopMode | |
| 24 | - * @since 0.20.0 | |
| 23 | + * @package OpenStation | |
| 25 | 24 | */ |
| 26 | 25 | |
| 27 | 26 | defined( 'ABSPATH' ) || exit; |
| 28 | 27 | |
| 29 | 28 | /** |
| 30 | - * Compute the summary payload for one user. Cheap enough to call | |
| 31 | - * per-row (each branch is one indexed query); WP's object cache | |
| 32 | - * absorbs repeats inside a single request. | |
| 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 | 33 | * |
| 34 | - * @since 0.20.0 | |
| 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. | |
| 35 | 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 | + * | |
| 36 | 154 | * @param int $user_id User id. |
| 37 | 155 | * @return array{postCount:int,roleLabels:array<int,string>,registered:string,lastActive:string} |
| 38 | 156 | */ |
| 39 | -function desktop_mode_my_wordpress_user_summary_payload( $user_id ) { | |
| 157 | +function openstation_my_wordpress_user_summary_payload( $user_id ) { | |
| 40 | 158 | $user_id = (int) $user_id; |
| 41 | 159 | if ( $user_id <= 0 ) { |
| 42 | 160 | return array( |
| 43 | 161 | 'postCount' => 0, |
| @@ -59,12 +177,17 @@ | ||
| 59 | 177 | |
| 60 | 178 | $can_see_private = current_user_can( 'list_users' ) |
| 61 | 179 | || ( get_current_user_id() === $user_id ); |
| 62 | 180 | |
| 181 | + $primed = openstation_my_wordpress_user_summary_cache(); | |
| 182 | + $types = openstation_my_wordpress_user_summary_post_types(); | |
| 183 | + | |
| 63 | 184 | // `count_user_posts` accepts an array of post types since 4.5; we |
| 64 | 185 | // limit to public ones the dossier already counts so the tile and |
| 65 | 186 | // the dossier agree. |
| 66 | - $post_count = (int) count_user_posts( $user_id, array( 'post', 'page' ), true ); | |
| 187 | + $post_count = isset( $primed[ $user_id ] ) | |
| 188 | + ? (int) $primed[ $user_id ]['postCount'] | |
| 189 | + : (int) count_user_posts( $user_id, $types, true ); | |
| 67 | 190 | |
| 68 | 191 | $role_labels = array(); |
| 69 | 192 | if ( $can_see_private && function_exists( 'wp_roles' ) ) { |
| 70 | 193 | $wp_roles = wp_roles(); |
| @@ -82,20 +205,27 @@ | ||
| 82 | 205 | |
| 83 | 206 | // Latest published post — useful at-a-glance "active recently?" |
| 84 | 207 | // signal for the tile. Drawn from `post_date_gmt` so timezone |
| 85 | 208 | // drift can't shift the displayed date. |
| 86 | - global $wpdb; | |
| 87 | - $last_active_raw = $wpdb->get_var( | |
| 88 | - $wpdb->prepare( | |
| 89 | - "SELECT MAX(post_date_gmt) FROM {$wpdb->posts} | |
| 90 | - WHERE post_author = %d | |
| 91 | - AND post_status = 'publish' | |
| 92 | - AND post_type IN ( 'post', 'page' )", | |
| 93 | - $user_id | |
| 94 | - ) | |
| 95 | - ); | |
| 96 | - $last_active = $last_active_raw ? mysql2date( 'c', $last_active_raw, false ) : ''; | |
| 209 | + if ( isset( $primed[ $user_id ] ) ) { | |
| 210 | + $last_active = (string) $primed[ $user_id ]['lastActive']; | |
| 211 | + } else { | |
| 212 | + global $wpdb; | |
| 97 | 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 | + | |
| 98 | 228 | return array( |
| 99 | 229 | 'postCount' => $post_count, |
| 100 | 230 | 'roleLabels' => $role_labels, |
| 101 | 231 | 'registered' => (string) $registered, |
| @@ -106,22 +236,20 @@ | ||
| 106 | 236 | /** |
| 107 | 237 | * Register the field on the `user` REST resource. The field is |
| 108 | 238 | * read-only (no `update_callback`); `get_callback` receives the |
| 109 | 239 | * user response array, from which we read `id`. |
| 110 | - * | |
| 111 | - * @since 0.20.0 | |
| 112 | 240 | */ |
| 113 | -function desktop_mode_my_wordpress_register_user_summary_field() { | |
| 241 | +function openstation_my_wordpress_register_user_summary_field() { | |
| 114 | 242 | register_rest_field( |
| 115 | 243 | 'user', |
| 116 | - 'desktop_mode_summary', | |
| 244 | + 'openstation_summary', | |
| 117 | 245 | array( |
| 118 | 246 | 'get_callback' => static function ( $user ) { |
| 119 | 247 | $id = isset( $user['id'] ) ? (int) $user['id'] : 0; |
| 120 | - return desktop_mode_my_wordpress_user_summary_payload( $id ); | |
| 248 | + return openstation_my_wordpress_user_summary_payload( $id ); | |
| 121 | 249 | }, |
| 122 | 250 | 'schema' => array( |
| 123 | - 'description' => __( 'Compact user summary for My WordPress.', 'desktop-mode' ), | |
| 251 | + 'description' => __( 'Compact user summary for the WP Explorer window.', 'desktop-mode' ), | |
| 124 | 252 | 'type' => 'object', |
| 125 | 253 | 'context' => array( 'view', 'edit', 'embed' ), |
| 126 | 254 | 'readonly' => true, |
| 127 | 255 | 'properties' => array( |
| @@ -146,5 +274,5 @@ | ||
| 146 | 274 | ), |
| 147 | 275 | ) |
| 148 | 276 | ); |
| 149 | 277 | } |
| 150 | -add_action( 'rest_api_init', 'desktop_mode_my_wordpress_register_user_summary_field' ); | |
| 278 | +add_action( 'rest_api_init', 'openstation_my_wordpress_register_user_summary_field' ); | |