PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-stats-admin/src/class-admin-post-list-column.php +379 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,379 @@
1 +<?php
2 +/**
3 + * A class that adds a stats column to wp-admin Post List.
4 + *
5 + * @package automattic/jetpack-stats-admin
6 + */
7 +
8 +namespace Automattic\Jetpack\Stats_Admin;
9 +
10 +use Automattic\Jetpack\Connection\Manager as Connection_Manager;
11 +use Automattic\Jetpack\Redirect;
12 +use Automattic\Jetpack\Stats\Options as Stats_Options;
13 +use Automattic\Jetpack\Stats\WPCOM_Stats;
14 +use Automattic\Jetpack\Status\Host;
15 +use NumberFormatter;
16 +
17 +/**
18 + * Add a Stats column in the post and page lists.
19 + */
20 +class Admin_Post_List_Column {
21 +
22 + /**
23 + * Create the object.
24 + *
25 + * @return self
26 + */
27 + public static function register() {
28 + return new self();
29 + }
30 +
31 + /**
32 + * A list of NumberFormatters.
33 + *
34 + * @var \NumberFormatter[]
35 + */
36 + private $formatter;
37 +
38 + /**
39 + * The current locale.
40 + *
41 + * @var string
42 + */
43 + private $locale;
44 +
45 + /**
46 + * The constructor.
47 + */
48 + public function __construct() {
49 + // Add an icon to see stats in WordPress.com for a particular post.
50 + add_action( 'admin_enqueue_scripts', array( $this, 'stats_load_admin_css' ) );
51 +
52 + add_filter( 'manage_posts_columns', array( $this, 'add_stats_post_table' ) );
53 + add_filter( 'manage_pages_columns', array( $this, 'add_stats_post_table' ) );
54 +
55 + add_action( 'manage_posts_custom_column', array( $this, 'add_stats_post_table_cell' ), 10, 2 );
56 + add_action( 'manage_pages_custom_column', array( $this, 'add_stats_post_table_cell' ), 10, 2 );
57 + }
58 +
59 + /**
60 + * Load CSS needed for Stats column width in WP-Admin area.
61 + *
62 + * @since 4.7.0
63 + * @since 0.33.0 Added the `$hook_suffix` parameter.
64 + *
65 + * @param string $hook_suffix The current admin page.
66 + */
67 + public function stats_load_admin_css( $hook_suffix = '' ) {
68 + if ( 'edit.php' !== $hook_suffix ) {
69 + return;
70 + }
71 +
72 + wp_add_inline_style(
73 + 'common',
74 + '.wp-list-table.fixed .column-stats { width: 9em; white-space: nowrap; }'
75 + );
76 + }
77 +
78 + /**
79 + * Set content for cell with link to an entry's stats in Odyssey Stats.
80 + *
81 + * @param string $column The name of the column to display.
82 + * @param int $post_id The current post ID.
83 + *
84 + * @since 4.7.0
85 + */
86 + public function add_stats_post_table_cell( $column, $post_id ) {
87 + if ( 'stats' === $column ) {
88 + if ( 'publish' !== get_post_status( $post_id ) ) {
89 + printf(
90 + '<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>',
91 + esc_html__( 'No stats', 'jetpack-stats-admin' )
92 + );
93 + } else {
94 + // Link to the wp-admin stats page.
95 + $query_args = array(
96 + 'from' => 'postList',
97 + 'jp_post_type' => get_post_type( $post_id ),
98 + );
99 +
100 + $list_criteria_params = array(
101 + 's' => sanitize_text_field( get_search_query() ),
102 + 'paged' => absint( get_query_var( 'paged' ) ),
103 + 'post_status' => sanitize_text_field( get_query_var( 'post_status' ) ),
104 + 'orderby' => sanitize_text_field( get_query_var( 'orderby' ) ),
105 + 'order' => sanitize_text_field( get_query_var( 'order' ) ),
106 + 'author' => absint( get_query_var( 'author' ) ),
107 + 'cat' => absint( get_query_var( 'cat' ) ), // 'cat' is the query var for category ID
108 + 'm' => absint( get_query_var( 'm' ) ), // 'm' is the query var for YYYYMM
109 + 'category_name' => sanitize_text_field( get_query_var( 'category_name' ) ),
110 + );
111 +
112 + foreach ( $list_criteria_params as $key => $value ) {
113 + if ( isset( $_GET[ $key ] ) && $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking if the key existts and not reading the value from the request.
114 + $query_args[ 'jp_' . $key ] = $value;
115 + }
116 + }
117 +
118 + $stats_post_url = add_query_arg( $query_args, admin_url( 'admin.php?page=stats#!/stats/post/' . $post_id . '/' . \Jetpack_Options::get_option( 'id', 0 ) ) );
119 + // Unless the user is on a Default style WOA site, in which case link to Calypso.
120 + if ( ( new Host() )->is_woa_site() && Stats_Options::get_option( 'enable_odyssey_stats' ) && 'wp-admin' !== get_option( 'wpcom_admin_interface' ) ) {
121 + $stats_post_url = Redirect::get_url(
122 + 'calypso-stats-post',
123 + array(
124 + 'path' => $post_id,
125 + )
126 + );
127 + }
128 +
129 + /**
130 + * Filters where the post list table's views column links to.
131 + *
132 + * Lets a newer analytics dashboard claim the entry point without this
133 + * package knowing about it.
134 + *
135 + * @since 0.34.0
136 + *
137 + * @param string $stats_post_url Stats URL for the post.
138 + * @param int $post_id The post the row belongs to.
139 + */
140 + $stats_post_url = apply_filters( 'jetpack_stats_post_list_column_url', $stats_post_url, $post_id );
141 +
142 + static $post_views = null;
143 +
144 + /**
145 + * Jetpack_stats_get_post_page_views_for_current_list makes a request with all post ids in the current $wp_query.
146 + * This way, we'll make a single API request instead of making one for each post.
147 + *
148 + * For this reason, we'll cache the result with the static $post_views variable.
149 + */
150 + if ( null === $post_views ) {
151 + $post_views = $this->get_post_page_views_for_current_list();
152 + }
153 +
154 + $views = $post_views[ $post_id ] ?? null;
155 +
156 + $current_locale = get_locale();
157 +
158 + if ( null !== $views ) {
159 + $formatted_views = class_exists( '\NumberFormatter' )
160 + ? $this->get_formatter( $current_locale )->format( $views )
161 + : $this->get_fallback_format_to_compact_version( $views );
162 + } else {
163 + $formatted_views = '';
164 + }
165 +
166 + ?>
167 + <a href="<?php echo esc_url( $stats_post_url ); ?>"
168 + title="<?php echo esc_html__( 'Views for the last thirty days. Click for detailed stats', 'jetpack-stats-admin' ); ?>">
169 + <span
170 + class="dashicons dashicons-visibility"></span>&nbsp;<span><?php echo null !== $views ? esc_html( $formatted_views ) : ''; ?></span>
171 + </a>
172 + <?php
173 + }
174 + }
175 + }
176 +
177 + /**
178 + * Set header for column that allows to view an entry's stats.
179 + *
180 + * @param array $columns An array of column names.
181 + *
182 + * @return mixed
183 + */
184 + public function add_stats_post_table( $columns ) {
185 + // Skip stats column for non-public post types when screen info is available.
186 + if ( function_exists( 'get_current_screen' ) ) {
187 + $screen = get_current_screen();
188 + if ( $screen && $screen->post_type ) {
189 + $post_type_object = get_post_type_object( $screen->post_type );
190 + if ( $post_type_object && ! $post_type_object->public ) {
191 + return $columns;
192 + }
193 + }
194 + }
195 +
196 + /**
197 + * The manage_options capability is a fallback for Simple.
198 + * This should be updated with a proper fix. Implemented based on this PR: https://github.com/Automattic/jetpack/pull/41549.
199 + */
200 + $has_access = current_user_can( 'view_stats' ) || current_user_can( 'manage_options' );
201 +
202 + /*
203 + * Stats can be accessed in wp-admin or in Calypso,
204 + * depending on what version of the stats screen is enabled on your site.
205 + *
206 + * In both cases, the user must be allowed to access stats.
207 + *
208 + * If the Odyssey Stats experience isn't enabled, the user will need to go to Calypso,
209 + * so they need to be connected to WordPress.com to be able to access that page.
210 + */
211 + if (
212 + ! $has_access
213 + || (
214 + ! Stats_Options::get_option( 'enable_odyssey_stats' )
215 + && ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected()
216 + )
217 + ) {
218 + return $columns;
219 + }
220 +
221 + // Array-Fu to add before comments.
222 + $pos = array_search( 'comments', array_keys( $columns ), true );
223 +
224 + // Fallback to the last position if the post type does not support comments.
225 + if ( ! is_int( $pos ) ) {
226 + $pos = count( $columns );
227 + }
228 +
229 + // If comments position is 0, then prepend the element at the beginning of the array.
230 + if ( 0 === $pos ) {
231 + return array_merge(
232 + array( 'stats' => esc_html__( 'Views: 30 days', 'jetpack-stats-admin' ) ),
233 + $columns
234 + );
235 + }
236 +
237 + $chunks = array_chunk( $columns, $pos, true );
238 + $chunks[0]['stats'] = esc_html__( 'Views: 30 days', 'jetpack-stats-admin' );
239 +
240 + return call_user_func_array( 'array_merge', $chunks );
241 + }
242 +
243 + /**
244 + * Get a list of post views for each post id from the global $wp_query.
245 + *
246 + * @return array
247 + */
248 + public function get_post_page_views_for_current_list(): array {
249 + global $wp_query;
250 +
251 + if ( $wp_query->posts ) {
252 + $post_ids = wp_list_pluck( $wp_query->posts, 'ID' );
253 + } elseif ( wp_doing_ajax() && ! empty( $_POST['action'] ) && 'inline-save' === $_POST['action'] && ! empty( $_POST['post_ID'] ) && check_ajax_referer( 'inlineeditnonce', '_inline_edit' ) ) {
254 + $post_ids = array( absint( wp_unslash( $_POST['post_ID'] ) ) );
255 + } else {
256 + return array();
257 + }
258 +
259 + $wpcom_stats = $this->get_stats();
260 + $post_views = $wpcom_stats->get_total_post_views(
261 + array(
262 + 'num' => 30,
263 + 'post_ids' => implode( ',', $post_ids ),
264 + )
265 + );
266 +
267 + if ( is_wp_error( $post_views ) || empty( $post_views ) ) {
268 + return array();
269 + }
270 +
271 + $views = array();
272 +
273 + foreach ( $post_views['posts'] as $post ) {
274 + $views[ $post['ID'] ] = $post['views'];
275 + }
276 +
277 + return $views;
278 + }
279 +
280 + /**
281 + * Get the stats object.
282 + *
283 + * @return WPCOM_Stats
284 + */
285 + protected function get_stats() {
286 + return new WPCOM_Stats();
287 + }
288 +
289 + /**
290 + * Get and validate the locale.
291 + *
292 + * @param string $locale The locale to validate.
293 + *
294 + * @return string The validated locale.
295 + */
296 + public function get_validated_locale( string $locale ): string {
297 + if ( isset( $this->locale ) ) {
298 + return $this->locale;
299 + }
300 +
301 + /*
302 + * Check if the locale is valid and available.
303 + * If not, fallback to en_US.
304 + */
305 + if ( ! in_array( $locale, \IntlCalendar::getAvailableLocales(), true ) ) {
306 + $locale = 'en_US';
307 + }
308 +
309 + $this->locale = $locale;
310 + return $locale;
311 + }
312 +
313 + /**
314 + * Get the NumberFormatter instance.
315 + *
316 + * @param string $locale The current locale.
317 + *
318 + * @return NumberFormatter
319 + */
320 + protected function get_formatter( string $locale ): \NumberFormatter {
321 + if ( isset( $this->formatter[ $locale ] ) ) {
322 + return $this->formatter[ $locale ];
323 + }
324 +
325 + $locale = $this->get_validated_locale( $locale );
326 +
327 + /**
328 + * PHP's NumberFormatter is just a wrapper over the ICU C library. The library does support decimal compact short formatter, but PHP doesn't have a stub for it (=< PHP 8.4).
329 + *
330 + * @see https://unicode-org.github.io/icu-docs/apidoc/dev/icu4c/unum_8h.html UNUM_DECIMAL_COMPACT_SHORT constant.
331 + */
332 + $compact_decimal_short = 14;
333 +
334 + /**
335 + * NumberFormatter::DECIMAL_COMPACT_SHORT only exists in PHP 8.5 and later. At this time, NumberFormatter::DECIMAL_COMPACT_SHORT only exists in PHP `main` branch.
336 + *
337 + * Use the constant if it's defined since it's safer.
338 + */
339 + if ( defined( '\NumberFormatter::DECIMAL_COMPACT_SHORT' ) ) {
340 + // @phan-suppress-next-line PhanUndeclaredConstantOfClass
341 + $compact_decimal_short = NumberFormatter::DECIMAL_COMPACT_SHORT;
342 + }
343 +
344 + try {
345 + $formatter = new \NumberFormatter( $locale, $compact_decimal_short );
346 + $formatter->setAttribute( \NumberFormatter::MAX_FRACTION_DIGITS, 1 );
347 + } catch ( \Exception $e ) {
348 + // Fallback to decimal if for some reason it fails to work.
349 + $formatter = new \NumberFormatter( $locale, \NumberFormatter::DECIMAL );
350 + }
351 +
352 + $this->formatter[ $locale ] = $formatter;
353 +
354 + return $formatter;
355 + }
356 +
357 + /**
358 + * Fallback Format a number to a compact version if the Intl extension is not available.
359 + *
360 + * @param int $views The given number.
361 + *
362 + * @return string
363 + */
364 + public function get_fallback_format_to_compact_version( $views ) {
365 + if ( $views >= 10000000 ) {
366 + return round( $views / 1000000 ) . 'M';
367 + } elseif ( $views >= 1000000 ) {
368 + $views = round( $views / 1000000, 1 );
369 + return preg_replace( '/\.0$/', '', (string) $views ) . 'M';
370 + } elseif ( $views >= 10000 ) {
371 + return round( $views / 1000 ) . 'K';
372 + } elseif ( $views >= 1000 ) {
373 + $views = round( $views / 1000, 1 );
374 + return preg_replace( '/\.0$/', '', (string) $views ) . 'K';
375 + }
376 +
377 + return (string) $views;
378 + }
379 +}