PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
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 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-stats-admin / src / class-admin-post-list-column.php

class-admin-post-list-column.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at jetpack_vendor/automattic/jetpack-stats-admin/src/class-admin-post-list-column.php

380 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
380