PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.9
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.9
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 0.8.6 All 33 releases
desktop-mode / includes / widgets / widget-post-stats.php

widget-post-stats.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.9, at includes/widgets/widget-post-stats.php

206 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Post Stats Widget.
4 *
5 * Bar chart of posts published per month for the last 6 months,
6 * broken down by published / draft / pending status.
7 *
8 * Refresh: every 5 minutes.
9 * Requires: OpenStation 0.18.0+ (openstation_register_widget).
10 *
11 * @package OpenStation
12 */
13
14 defined( 'ABSPATH' ) || exit;
15
16 /**
17 * Register the REST endpoint that aggregates per-month post counts
18 * server-side. Replaces the widget's original client-side approach —
19 * 3 statuses × up-to-100-per-page `/wp/v2/posts` requests every
20 * refresh — with a single GROUP BY that's shared through a transient.
21 *
22 * Route: GET /desktop-mode/v1/post-stats
23 * Permission: edit_posts.
24 */
25 function openstation_register_post_stats_rest_route() {
26 register_rest_route(
27 'desktop-mode/v1',
28 '/post-stats',
29 array(
30 'methods' => WP_REST_Server::READABLE,
31 'callback' => 'openstation_post_stats_callback',
32 'permission_callback' => static function () {
33 return current_user_can( 'edit_posts' );
34 },
35 )
36 );
37 }
38 add_action( 'rest_api_init', 'openstation_register_post_stats_rest_route' );
39
40 /**
41 * Aggregate post counts per month × status for the last 6 months.
42 *
43 * Capability scoping mirrors what the widget's old `/wp/v2/posts`
44 * queries returned: published posts count site-wide for anyone with
45 * `edit_posts`, while draft / pending counts are scoped to the
46 * current user's own posts unless they hold `edit_others_posts`
47 * (core's REST posts controller applies the same visibility).
48 *
49 * Cached for 5 minutes per scope — the data is a trailing-6-month
50 * aggregate, the widget refreshes every 5 minutes, and every viewer
51 * in the same capability scope can share one computation.
52 *
53 * @return array{months:array<int,array{ym:string,publish:int,draft:int,pending:int}>}
54 */
55 function openstation_post_stats_callback() {
56 global $wpdb;
57
58 $see_others = current_user_can( 'edit_others_posts' );
59 $cache_key = $see_others
60 ? 'desktop_mode_post_stats_all'
61 : 'desktop_mode_post_stats_own_' . get_current_user_id();
62
63 $cached = get_transient( $cache_key );
64 if ( is_array( $cached ) ) {
65 return $cached;
66 }
67
68 $months_back = 6;
69 // First day of the earliest bucket, site timezone.
70 $cutoff = gmdate(
71 'Y-m-01 00:00:00',
72 strtotime( current_time( 'Y-m-01' ) . ' -' . ( $months_back - 1 ) . ' months' )
73 );
74
75 // Two literal query branches (rather than a concatenated author
76 // clause) so every byte of SQL inside prepare() is static —
77 // keeps the PreparedSQL sniff able to verify it.
78 if ( $see_others ) {
79 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- single aggregate GROUP BY, result cached in the transient below.
80 $rows = $wpdb->get_results(
81 $wpdb->prepare(
82 "SELECT DATE_FORMAT( post_date, '%%Y-%%m' ) AS ym, post_status, COUNT(*) AS cnt
83 FROM {$wpdb->posts}
84 WHERE post_type = %s
85 AND post_status IN ( 'publish', 'draft', 'pending' )
86 AND post_date >= %s
87 GROUP BY ym, post_status",
88 'post',
89 $cutoff
90 ),
91 ARRAY_A
92 );
93 } else {
94 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- single aggregate GROUP BY, result cached in the transient below.
95 $rows = $wpdb->get_results(
96 $wpdb->prepare(
97 "SELECT DATE_FORMAT( post_date, '%%Y-%%m' ) AS ym, post_status, COUNT(*) AS cnt
98 FROM {$wpdb->posts}
99 WHERE post_type = %s
100 AND post_status IN ( 'publish', 'draft', 'pending' )
101 AND post_date >= %s
102 AND ( post_status = %s OR post_author = %d )
103 GROUP BY ym, post_status",
104 'post',
105 $cutoff,
106 'publish',
107 get_current_user_id()
108 ),
109 ARRAY_A
110 );
111 }
112
113 // Emit exactly $months_back buckets, oldest first, zero-filled —
114 // the widget renders a fixed axis and shouldn't have to guess at
115 // missing months.
116 $buckets = array();
117 for ( $i = $months_back - 1; $i >= 0; $i-- ) {
118 $ym = gmdate( 'Y-m', strtotime( current_time( 'Y-m-01' ) . ' -' . $i . ' months' ) );
119 $buckets[ $ym ] = array(
120 'ym' => $ym,
121 'publish' => 0,
122 'draft' => 0,
123 'pending' => 0,
124 );
125 }
126 foreach ( (array) $rows as $row ) {
127 $ym = isset( $row['ym'] ) ? (string) $row['ym'] : '';
128 $status = isset( $row['post_status'] ) ? (string) $row['post_status'] : '';
129 if ( isset( $buckets[ $ym ][ $status ] ) ) {
130 $buckets[ $ym ][ $status ] = (int) $row['cnt'];
131 }
132 }
133
134 $result = array( 'months' => array_values( $buckets ) );
135
136 set_transient( $cache_key, $result, 5 * MINUTE_IN_SECONDS );
137
138 return $result;
139 }
140
141 /**
142 * Register the JS + CSS assets.
143 */
144 function openstation_register_post_stats_widget_assets() {
145 $suffix = openstation_asset_suffix();
146 $version = defined( 'OPENSTATION_VERSION' ) ? OPENSTATION_VERSION : '0';
147
148 $js_path = OPENSTATION_DIR . 'assets/js/widget-post-stats' . $suffix . '.js';
149 $css_path = OPENSTATION_DIR . 'assets/js/widget-post-stats' . $suffix . '.css';
150
151 wp_register_style(
152 'os-post-stats-widget',
153 OPENSTATION_URL . 'assets/js/widget-post-stats' . $suffix . '.css',
154 array(),
155 file_exists( $css_path ) ? (string) filemtime( $css_path ) : $version
156 );
157
158 wp_register_script(
159 'os-post-stats-widget',
160 OPENSTATION_URL . 'assets/js/widget-post-stats' . $suffix . '.js',
161 array( 'wp-api-fetch' ),
162 file_exists( $js_path ) ? (string) filemtime( $js_path ) : $version,
163 true
164 );
165 }
166 add_action( 'init', 'openstation_register_post_stats_widget_assets', 5 );
167
168 /**
169 * Eagerly enqueue the CSS on shell pages.
170 */
171 function openstation_enqueue_post_stats_widget_styles() {
172 if ( function_exists( 'openstation_is_enabled' ) && ! openstation_is_enabled() ) {
173 return;
174 }
175 if ( function_exists( 'openstation_is_chromeless_request' ) && openstation_is_chromeless_request() ) {
176 return;
177 }
178 wp_enqueue_style( 'os-post-stats-widget' );
179 }
180 add_action( 'admin_enqueue_scripts', 'openstation_enqueue_post_stats_widget_styles', 20 );
181
182 /**
183 * Register the widget definition.
184 */
185 function openstation_register_post_stats_widget() {
186 if ( ! function_exists( 'openstation_register_widget' ) ) {
187 return;
188 }
189 openstation_register_widget(
190 'desktop-mode/post-stats',
191 array(
192 'label' => __( 'Post Stats', 'desktop-mode' ),
193 'description' => __( 'Bar chart of posts per month over the last 6 months.', 'desktop-mode' ),
194 'icon' => 'dashicons-chart-bar',
195 'script' => 'os-post-stats-widget',
196 'movable' => true,
197 'resizable' => true,
198 'min_width' => 260,
199 'min_height' => 200,
200 'default_width' => 320,
201 'default_height' => 260,
202 )
203 );
204 }
205 add_action( 'init', 'openstation_register_post_stats_widget', 6 );
206