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 / term-stats.php

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

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