PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.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 / term-stats.php

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

348 lines 10.1 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-term stats endpoint.
4 *
5 * `GET /desktop-mode/v1/term-stats/<taxonomy>/<id>` returns an
6 * aggregated profile for a single category or tag — counts, recent
7 * posts in the term, top authors, co-occurring terms, 12-month
8 * activity sparkline, milestones. Powers the right preview pane in
9 * the My WordPress folder when a term is selected.
10 *
11 * Permissions: any logged-in user with `read` (default for most
12 * roles) — terms are public-facing data on the WP site, so the same
13 * cap that lets you read the front-end is enough to inspect their
14 * stats. Author archives are also public so listing top authors is
15 * not new disclosure.
16 *
17 * @package OpenStation
18 */
19
20 defined( 'ABSPATH' ) || exit;
21
22 /**
23 * Register the route.
24 */
25 function openstation_my_wordpress_register_term_stats_route() {
26 register_rest_route(
27 'desktop-mode/v1',
28 '/term-stats/(?P<taxonomy>[a-zA-Z0-9_-]+)/(?P<id>\d+)',
29 array(
30 'methods' => WP_REST_Server::READABLE,
31 'callback' => 'openstation_my_wordpress_term_stats_callback',
32 'permission_callback' => static function () {
33 return is_user_logged_in() && current_user_can( 'read' );
34 },
35 'args' => array(
36 'taxonomy' => array(
37 'required' => true,
38 'type' => 'string',
39 'sanitize_callback' => 'sanitize_key',
40 ),
41 'id' => array(
42 'required' => true,
43 'type' => 'integer',
44 'sanitize_callback' => 'absint',
45 ),
46 ),
47 )
48 );
49 }
50 add_action( 'rest_api_init', 'openstation_my_wordpress_register_term_stats_route' );
51
52 /**
53 * Aggregator callback. See file docblock for return shape.
54 *
55 * @param WP_REST_Request $request REST request.
56 * @return array|WP_Error
57 */
58 function openstation_my_wordpress_term_stats_callback( $request ) {
59 global $wpdb;
60 $taxonomy = sanitize_key( (string) $request->get_param( 'taxonomy' ) );
61 $term_id = (int) $request->get_param( 'id' );
62
63 $tax_obj = get_taxonomy( $taxonomy );
64 if ( ! $tax_obj ) {
65 return new WP_Error(
66 'openstation_invalid_taxonomy',
67 __( 'Unknown taxonomy.', 'desktop-mode' ),
68 array( 'status' => 400 )
69 );
70 }
71
72 $term = get_term( $term_id, $taxonomy );
73 if ( ! $term || is_wp_error( $term ) ) {
74 return new WP_Error(
75 'openstation_term_not_found',
76 __( 'Term not found.', 'desktop-mode' ),
77 array( 'status' => 404 )
78 );
79 }
80
81 // ----- Profile -----------------------------------------------------
82 $profile = array(
83 'id' => (int) $term->term_id,
84 'name' => $term->name,
85 'slug' => $term->slug,
86 'taxonomy' => $term->taxonomy,
87 'taxonomyLabel' => isset( $tax_obj->labels->singular_name )
88 ? (string) $tax_obj->labels->singular_name
89 : $taxonomy,
90 'description' => (string) $term->description,
91 'link' => get_term_link( $term ) instanceof WP_Error
92 ? ''
93 : (string) get_term_link( $term ),
94 'parent' => (int) $term->parent,
95 'storedCount' => (int) $term->count, // core's published-only count
96 );
97 if ( $term->parent > 0 ) {
98 $parent = get_term( $term->parent, $taxonomy );
99 if ( $parent && ! is_wp_error( $parent ) ) {
100 $profile['parentName'] = $parent->name;
101 }
102 }
103
104 $tt_id = (int) $term->term_taxonomy_id;
105
106 // ----- Counts ------------------------------------------------------
107 // Post-status breakdown for posts in this term.
108 $status_rows = $wpdb->get_results(
109 $wpdb->prepare(
110 "SELECT p.post_status, COUNT(DISTINCT p.ID) AS n
111 FROM {$wpdb->posts} p
112 INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
113 WHERE tr.term_taxonomy_id = %d
114 AND p.post_type = 'post'
115 AND p.post_status NOT IN ( 'auto-draft', 'inherit', 'trash' )
116 GROUP BY p.post_status",
117 $tt_id
118 ),
119 ARRAY_A
120 );
121 $post_counts = array(
122 'publish' => 0,
123 'draft' => 0,
124 'pending' => 0,
125 'private' => 0,
126 'future' => 0,
127 'total' => 0,
128 );
129 foreach ( (array) $status_rows as $row ) {
130 $status = (string) $row['post_status'];
131 $n = (int) $row['n'];
132 $post_counts['total'] += $n;
133 if ( isset( $post_counts[ $status ] ) ) {
134 $post_counts[ $status ] = $n;
135 }
136 }
137
138 // Comments on posts in this term (approved only).
139 $comments_received = (int) $wpdb->get_var(
140 $wpdb->prepare(
141 "SELECT COUNT(c.comment_ID)
142 FROM {$wpdb->comments} c
143 INNER JOIN {$wpdb->posts} p ON c.comment_post_ID = p.ID
144 INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
145 WHERE tr.term_taxonomy_id = %d
146 AND p.post_status = 'publish'
147 AND c.comment_approved = '1'",
148 $tt_id
149 )
150 );
151
152 // Distinct authors using this term.
153 $distinct_authors = (int) $wpdb->get_var(
154 $wpdb->prepare(
155 "SELECT COUNT( DISTINCT p.post_author )
156 FROM {$wpdb->posts} p
157 INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
158 WHERE tr.term_taxonomy_id = %d
159 AND p.post_status = 'publish'",
160 $tt_id
161 )
162 );
163
164 $counts = array(
165 'posts' => $post_counts,
166 'commentsReceived' => $comments_received,
167 'distinctAuthors' => $distinct_authors,
168 );
169
170 // ----- Recent posts (5 most recent) --------------------------------
171 $recent_rows = $wpdb->get_results(
172 $wpdb->prepare(
173 "SELECT DISTINCT p.ID, p.post_title, p.post_date_gmt, p.post_status, p.post_type, p.post_author
174 FROM {$wpdb->posts} p
175 INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
176 WHERE tr.term_taxonomy_id = %d
177 AND p.post_status IN ( 'publish', 'private', 'future', 'draft', 'pending' )
178 AND p.post_type = 'post'
179 ORDER BY p.post_date_gmt DESC
180 LIMIT 5",
181 $tt_id
182 ),
183 ARRAY_A
184 );
185 $recent = array();
186 foreach ( (array) $recent_rows as $row ) {
187 $post_id = (int) $row['ID'];
188 $author_id = (int) $row['post_author'];
189 $author = $author_id > 0 ? get_userdata( $author_id ) : null;
190 $author_arr = $author
191 ? array(
192 'id' => (int) $author->ID,
193 'name' => $author->display_name,
194 'avatarUrl' => get_avatar_url( $author->ID, array( 'size' => 48 ) ),
195 )
196 : null;
197 $recent[] = array(
198 'id' => $post_id,
199 'title' => get_the_title( $post_id ),
200 'date' => mysql2date( 'c', (string) $row['post_date_gmt'], false ),
201 'status' => (string) $row['post_status'],
202 'type' => (string) $row['post_type'],
203 'link' => (string) get_permalink( $post_id ),
204 'author' => $author_arr,
205 );
206 }
207
208 // ----- Top authors (most posts in this term) -----------------------
209 $top_author_rows = $wpdb->get_results(
210 $wpdb->prepare(
211 "SELECT p.post_author, COUNT( DISTINCT p.ID ) AS n
212 FROM {$wpdb->posts} p
213 INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
214 WHERE tr.term_taxonomy_id = %d
215 AND p.post_status = 'publish'
216 AND p.post_author > 0
217 GROUP BY p.post_author
218 ORDER BY n DESC
219 LIMIT 5",
220 $tt_id
221 ),
222 ARRAY_A
223 );
224 $top_authors = array();
225 foreach ( (array) $top_author_rows as $row ) {
226 $user_id = (int) $row['post_author'];
227 $u = get_userdata( $user_id );
228 if ( ! $u ) {
229 continue;
230 }
231 $top_authors[] = array(
232 'userId' => (int) $u->ID,
233 'userName' => (string) $u->display_name,
234 'userAvatarUrl' => (string) get_avatar_url( $u->ID, array( 'size' => 48 ) ),
235 'count' => (int) $row['n'],
236 );
237 }
238
239 // ----- Co-occurring terms (most frequent siblings in same tax) -----
240 $co_term_rows = $wpdb->get_results(
241 $wpdb->prepare(
242 "SELECT t.term_id, t.name, t.slug, COUNT(*) AS n
243 FROM {$wpdb->term_relationships} tr1
244 INNER JOIN {$wpdb->term_relationships} tr2 ON tr1.object_id = tr2.object_id
245 INNER JOIN {$wpdb->term_taxonomy} tt2 ON tr2.term_taxonomy_id = tt2.term_taxonomy_id
246 INNER JOIN {$wpdb->terms} t ON tt2.term_id = t.term_id
247 INNER JOIN {$wpdb->posts} p ON p.ID = tr1.object_id
248 WHERE tr1.term_taxonomy_id = %d
249 AND tt2.taxonomy = %s
250 AND tt2.term_id != %d
251 AND p.post_status = 'publish'
252 GROUP BY t.term_id
253 ORDER BY n DESC
254 LIMIT 5",
255 $tt_id,
256 $taxonomy,
257 $term_id
258 ),
259 ARRAY_A
260 );
261 $co_terms = array();
262 foreach ( (array) $co_term_rows as $row ) {
263 $co_terms[] = array(
264 'id' => (int) $row['term_id'],
265 'name' => (string) $row['name'],
266 'slug' => (string) $row['slug'],
267 'count' => (int) $row['n'],
268 );
269 }
270
271 // ----- 12-month activity sparkline ---------------------------------
272 $activity_rows = $wpdb->get_results(
273 $wpdb->prepare(
274 "SELECT DATE_FORMAT( p.post_date_gmt, '%%Y-%%m' ) AS ym, COUNT( DISTINCT p.ID ) AS n
275 FROM {$wpdb->posts} p
276 INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
277 WHERE tr.term_taxonomy_id = %d
278 AND p.post_type = 'post'
279 AND p.post_status = 'publish'
280 AND p.post_date_gmt >= DATE_SUB( NOW(), INTERVAL 12 MONTH )
281 GROUP BY ym
282 ORDER BY ym ASC",
283 $tt_id
284 ),
285 ARRAY_A
286 );
287 $activity = array();
288 foreach ( (array) $activity_rows as $row ) {
289 $activity[] = array(
290 'ym' => (string) $row['ym'],
291 'count' => (int) $row['n'],
292 );
293 }
294
295 // ----- First & last post in this term ------------------------------
296 $first_post_date = $wpdb->get_var(
297 $wpdb->prepare(
298 "SELECT MIN( p.post_date_gmt )
299 FROM {$wpdb->posts} p
300 INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
301 WHERE tr.term_taxonomy_id = %d
302 AND p.post_status = 'publish'
303 AND p.post_type = 'post'",
304 $tt_id
305 )
306 );
307 $last_post_date = $wpdb->get_var(
308 $wpdb->prepare(
309 "SELECT MAX( p.post_date_gmt )
310 FROM {$wpdb->posts} p
311 INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
312 WHERE tr.term_taxonomy_id = %d
313 AND p.post_status = 'publish'
314 AND p.post_type = 'post'",
315 $tt_id
316 )
317 );
318 $milestones = array(
319 'firstPosted' => $first_post_date ? mysql2date( 'c', $first_post_date, false ) : null,
320 'lastPosted' => $last_post_date ? mysql2date( 'c', $last_post_date, false ) : null,
321 );
322
323 $payload = array(
324 'profile' => $profile,
325 'counts' => $counts,
326 'recent' => $recent,
327 'topAuthors' => $top_authors,
328 'coTerms' => $co_terms,
329 'activity' => $activity,
330 'milestones' => $milestones,
331 );
332
333 /**
334 * Filter the per-term stats payload before it returns to the
335 * My WordPress folder window.
336 *
337 * @param array $payload Stats payload.
338 * @param string $taxonomy Taxonomy slug.
339 * @param int $term_id Term id.
340 */
341 return apply_filters(
342 'openstation_my_wordpress_term_stats',
343 $payload,
344 $taxonomy,
345 $term_id
346 );
347 }
348