| @@ -1,81 +1,199 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | - * Desktop Mode — Post Stats Widget. | |
| 3 | + * OpenStation — Post Stats Widget. | |
| 4 | 4 | * |
| 5 | 5 | * Bar chart of posts published per month for the last 6 months, |
| 6 | 6 | * broken down by published / draft / pending status. |
| 7 | 7 | * |
| 8 | 8 | * Refresh: every 5 minutes. |
| 9 | - * Requires: Desktop Mode 0.18.0+ (desktop_mode_register_widget). | |
| 9 | + * Requires: OpenStation 0.18.0+ (openstation_register_widget). | |
| 10 | 10 | * |
| 11 | - * @package WPDesktopMode | |
| 12 | - * @since 0.26.0 | |
| 11 | + * @package OpenStation | |
| 13 | 12 | */ |
| 14 | 13 | |
| 15 | 14 | defined( 'ABSPATH' ) || exit; |
| 16 | 15 | |
| 17 | 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 | +/** | |
| 18 | 142 | * Register the JS + CSS assets. |
| 19 | - * | |
| 20 | - * @since 0.26.0 | |
| 21 | 143 | */ |
| 22 | -function desktop_mode_register_post_stats_widget_assets() { | |
| 23 | - $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; | |
| 24 | - $version = defined( 'DESKTOP_MODE_VERSION' ) ? DESKTOP_MODE_VERSION : '0'; | |
| 144 | +function openstation_register_post_stats_widget_assets() { | |
| 145 | + $suffix = openstation_asset_suffix(); | |
| 146 | + $version = defined( 'OPENSTATION_VERSION' ) ? OPENSTATION_VERSION : '0'; | |
| 25 | 147 | |
| 26 | - $js_path = DESKTOP_MODE_DIR . 'assets/js/widget-post-stats' . $suffix . '.js'; | |
| 27 | - $css_path = DESKTOP_MODE_DIR . 'assets/js/widget-post-stats' . $suffix . '.css'; | |
| 148 | + $js_path = OPENSTATION_DIR . 'assets/js/widget-post-stats' . $suffix . '.js'; | |
| 149 | + $css_path = OPENSTATION_DIR . 'assets/js/widget-post-stats' . $suffix . '.css'; | |
| 28 | 150 | |
| 29 | 151 | wp_register_style( |
| 30 | - 'desktop-mode-post-stats-widget', | |
| 31 | - DESKTOP_MODE_URL . 'assets/js/widget-post-stats' . $suffix . '.css', | |
| 152 | + 'os-post-stats-widget', | |
| 153 | + OPENSTATION_URL . 'assets/js/widget-post-stats' . $suffix . '.css', | |
| 32 | 154 | array(), |
| 33 | 155 | file_exists( $css_path ) ? (string) filemtime( $css_path ) : $version |
| 34 | 156 | ); |
| 35 | 157 | |
| 36 | 158 | wp_register_script( |
| 37 | - 'desktop-mode-post-stats-widget', | |
| 38 | - DESKTOP_MODE_URL . 'assets/js/widget-post-stats' . $suffix . '.js', | |
| 159 | + 'os-post-stats-widget', | |
| 160 | + OPENSTATION_URL . 'assets/js/widget-post-stats' . $suffix . '.js', | |
| 39 | 161 | array( 'wp-api-fetch' ), |
| 40 | 162 | file_exists( $js_path ) ? (string) filemtime( $js_path ) : $version, |
| 41 | 163 | true |
| 42 | 164 | ); |
| 43 | 165 | } |
| 44 | -add_action( 'init', 'desktop_mode_register_post_stats_widget_assets', 5 ); | |
| 166 | +add_action( 'init', 'openstation_register_post_stats_widget_assets', 5 ); | |
| 45 | 167 | |
| 46 | 168 | /** |
| 47 | 169 | * Eagerly enqueue the CSS on shell pages. |
| 48 | - * | |
| 49 | - * @since 0.26.0 | |
| 50 | 170 | */ |
| 51 | -function desktop_mode_enqueue_post_stats_widget_styles() { | |
| 52 | - if ( function_exists( 'desktop_mode_is_enabled' ) && ! desktop_mode_is_enabled() ) { | |
| 171 | +function openstation_enqueue_post_stats_widget_styles() { | |
| 172 | + if ( function_exists( 'openstation_is_enabled' ) && ! openstation_is_enabled() ) { | |
| 53 | 173 | return; |
| 54 | 174 | } |
| 55 | - if ( function_exists( 'desktop_mode_is_chromeless_request' ) && desktop_mode_is_chromeless_request() ) { | |
| 175 | + if ( function_exists( 'openstation_is_chromeless_request' ) && openstation_is_chromeless_request() ) { | |
| 56 | 176 | return; |
| 57 | 177 | } |
| 58 | - wp_enqueue_style( 'desktop-mode-post-stats-widget' ); | |
| 178 | + wp_enqueue_style( 'os-post-stats-widget' ); | |
| 59 | 179 | } |
| 60 | -add_action( 'admin_enqueue_scripts', 'desktop_mode_enqueue_post_stats_widget_styles', 20 ); | |
| 180 | +add_action( 'admin_enqueue_scripts', 'openstation_enqueue_post_stats_widget_styles', 20 ); | |
| 61 | 181 | |
| 62 | 182 | /** |
| 63 | 183 | * Register the widget definition. |
| 64 | - * | |
| 65 | - * @since 0.26.0 | |
| 66 | 184 | */ |
| 67 | -function desktop_mode_register_post_stats_widget() { | |
| 68 | - if ( ! function_exists( 'desktop_mode_register_widget' ) ) { | |
| 185 | +function openstation_register_post_stats_widget() { | |
| 186 | + if ( ! function_exists( 'openstation_register_widget' ) ) { | |
| 69 | 187 | return; |
| 70 | 188 | } |
| 71 | - desktop_mode_register_widget( | |
| 189 | + openstation_register_widget( | |
| 72 | 190 | 'desktop-mode/post-stats', |
| 73 | 191 | array( |
| 74 | 192 | 'label' => __( 'Post Stats', 'desktop-mode' ), |
| 75 | 193 | 'description' => __( 'Bar chart of posts per month over the last 6 months.', 'desktop-mode' ), |
| 76 | 194 | 'icon' => 'dashicons-chart-bar', |
| 77 | - 'script' => 'desktop-mode-post-stats-widget', | |
| 195 | + 'script' => 'os-post-stats-widget', | |
| 78 | 196 | 'movable' => true, |
| 79 | 197 | 'resizable' => true, |
| 80 | 198 | 'min_width' => 260, |
| 81 | 199 | 'min_height' => 200, |
| @@ -83,5 +201,5 @@ | ||
| 83 | 201 | 'default_height' => 260, |
| 84 | 202 | ) |
| 85 | 203 | ); |
| 86 | 204 | } |
| 87 | -add_action( 'init', 'desktop_mode_register_post_stats_widget', 6 ); | |
| 205 | +add_action( 'init', 'openstation_register_post_stats_widget', 6 ); | |