PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.7
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 / widgets / widget-post-stats.php

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

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