<div id="dashboard_stats" class="is-loading">
<div style="height: 250px;"></div>
* Stats Dashboard Widget Content.
* TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
function stats_dashboard_widget_content() {
$width = isset( $_GET['width'] ) ? intval( $_GET['width'] ) / 2 : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$height = isset( $_GET['height'] ) ? intval( $_GET['height'] ) - 36 : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! $width || $width < 250 ) {
if ( ! $height || $height < 230 ) {
$options = stats_dashboard_widget_options();
$blog_id = Jetpack_Options::get_option( 'id' );
'unit' => $options['chart'],
'color' => get_user_option( 'admin_color' ),
'j' => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
$url = 'https://' . STATS_DASHBOARD_SERVER . '/wp-admin/index.php';
$url = add_query_arg( $q, $url );
$user_id = 0; // Means use the blog token.
$get = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
$get_code = wp_remote_retrieve_response_code( $get );
if ( is_wp_error( $get ) || $get_code === '' || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
stats_print_wp_remote_error( $get, $url );
$body = stats_convert_post_titles( $get['body'] );
$body = stats_convert_chart_urls( $body );
$body = stats_convert_image_urls( $body );
echo $body; // phpcs:ignore WordPress.Security.EscapeOutput
$csv_end_date = current_time( 'Y-m-d' );
'top' => "&limit=6&end=$csv_end_date",
'search' => "&limit=5&end=$csv_end_date",
$top_posts = stats_get_csv( 'postviews', "days=$options[top]$csv_args[top]" );
foreach ( $top_posts as $i => $post ) {
if ( 0 === $post['post_id'] ) {
unset( $top_posts[ $i ] );
$post_ids[] = $post['post_id'];
get_posts( array( 'include' => implode( ',', array_unique( $post_ids ) ) ) );
$search_terms = stats_get_csv( 'searchterms', "days=$options[search]$csv_args[search]" );
foreach ( $search_terms as $search_term ) {
if ( 'encrypted_search_terms' === $search_term['searchterm'] ) {
$searches[] = esc_html( $search_term['searchterm'] );
<div id="stats-info-container">
<div class="stats-info-header">
<h2><?php esc_html_e( 'Highlights', 'jetpack' ); ?></h2>
<div class="stats-info-header-right">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=stats' ) ); ?>" class="button button-primary">
<?php esc_html_e( 'View detailed stats', 'jetpack' ); ?>
<div class="stats-info-content">
<div id="top-posts" class="stats-section">
<div class="stats-section-inner">
<h3 class="heading"><?php esc_html_e( 'Top Posts', 'jetpack' ); ?></h3>
if ( empty( $top_posts ) ) {
<p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
foreach ( $top_posts as $post ) {
if ( ! get_post( $post['post_id'] ) ) {
/* Translators: Stats dashboard widget Post list with view count: "Post Title 1 View (or Views if plural)". */
_n( '%1$s %2$s View', '%1$s %2$s Views', $post['views'], 'jetpack' )
'<a href="' . esc_url( get_permalink( $post['post_id'] ) ) . '">' . esc_html( get_the_title( $post['post_id'] ) ) . '</a>',
esc_html( number_format_i18n( $post['views'] ) )
<div id="top-search" class="stats-section">
<div class="stats-section-inner">
<h3 class="heading"><?php esc_html_e( 'Top Searches', 'jetpack' ); ?></h3>
if ( empty( $searches ) ) {
<p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
foreach ( $searches as $search_term_item ) {
esc_html( $search_term_item )
* Stats Print WP Remote Error.
function stats_print_wp_remote_error( $get, $url ) {
$state_name = 'stats_remote_error_' . substr( md5( $url ), 0, 8 );
$previous_error = Jetpack::state( $state_name );
$error = md5( wp_json_encode( compact( 'get', 'url' ), JSON_UNESCAPED_SLASHES ) );
Jetpack::state( $state_name, $error );
if ( $error !== $previous_error ) {
<p><?php esc_html_e( 'We were unable to get your stats just now. Please reload this page to try again.', 'jetpack' ); ?></p>
/* translators: placeholder is an a href for a support site. */
esc_html__( 'We were unable to get your stats just now. Please reload this page to try again. If this error persists, please contact %1$s. In your report, please include the information below.', 'jetpack' ),
'<a href="https://support.wordpress.com/contact/?jetpack=needs-service">%s</a>',
esc_html__( 'Jetpack Support', 'jetpack' )
<pre class="stats-widget-error">
User Agent: "<?php echo isset( $_SERVER['HTTP_USER_AGENT'] ) ? esc_html( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ?>"
Page URL: "http<?php echo ( is_ssl() ? 's' : '' ) . '://' . esc_html( ( isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : '' ) . ( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ?>"
API URL: "<?php echo esc_url( $url ); ?>"
if ( is_wp_error( $get ) ) {
foreach ( $get->get_error_codes() as $code ) {
foreach ( $get->get_error_messages( $code ) as $message ) {
print esc_html( $code ) . ': "' . esc_html( $message ) . '"';
$get_code = wp_remote_retrieve_response_code( $get );
$content_length = strlen( wp_remote_retrieve_body( $get ) );
Response code: "<?php print esc_html( $get_code ); ?>"
Content length: "<?php print esc_html( $content_length ); ?>"
* Get stats from WordPress.com
* @param string $table The stats which you want to retrieve: postviews, or searchterms.
* An associative array of arguments.
* @type bool $end The last day of the desired time frame. Format is 'Y-m-d' (e.g. 2007-05-01)
* and default timezone is UTC date. Default value is Now.
* @type string $days The length of the desired time frame. Default is 30. Maximum 90 days.
* @type int $limit The maximum number of records to return. Default is 10. Maximum 100.
* @type int $post_id The ID of the post to retrieve stats data for
* @type string $summarize If present, summarizes all matching records. Default Null.
* @type int $blog_id The WordPress.com blog ID to retrieve stats data for. Default is the current blog.
* An array of post view data, each post as an array
* The post view data for a single post
* @type string $post_id The ID of the post
* @type string $post_title The title of the post
* @type string $post_permalink The permalink for the post
* @type string $views The number of views for the post within the $num_days specified
function stats_get_csv( $table, $args = null ) {
'blog_id' => Jetpack_Options::get_option( 'id' ),
$args = wp_parse_args( $args, $defaults );
$stats_csv_url = add_query_arg( $args, 'https://stats.wordpress.com/csv.php' );
$key = md5( $stats_csv_url );
$stats_cache = get_option( 'stats_cache' );
if ( ! $stats_cache || ! is_array( $stats_cache ) ) {
// Return or expire this key.
if ( isset( $stats_cache[ $key ] ) ) {
$time = key( $stats_cache[ $key ] );
if ( time() - $time < 300 ) {
return $stats_cache[ $key ][ $time ];
unset( $stats_cache[ $key ] );
$stats = stats_get_remote_csv( $stats_csv_url );
$labels = array_shift( $stats );
if ( 0 === stripos( $labels[0], 'error' ) ) {
for ( $s = 0; isset( $stats[ $s ] ); $s++ ) {
foreach ( $labels as $col => $label ) {
$row[ $label ] = $stats[ $s ][ $col ];
foreach ( $stats_cache as $k => $cache ) {
if ( ! is_array( $cache ) || 300 < time() - key( $cache ) ) {
unset( $stats_cache[ $k ] );
$stats_cache[ $key ] = array( time() => $stats_rows );
update_option( 'stats_cache', $stats_cache );
function stats_get_remote_csv( $url ) {
$user_id = 0; // Blog token.
$get = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
$get_code = wp_remote_retrieve_response_code( $get );
if ( is_wp_error( $get ) || $get_code === '' || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
return array(); // @todo: return an error?
return stats_str_getcsv( $get['body'] );
* Recursively run str_getcsv on the stats csv.
* @since 9.7.0 Remove custom handling since str_getcsv is available on all servers running this now.
function stats_str_getcsv( $csv ) {
// @todo Correctly handle embedded newlines. Note, despite claims online, `str_getcsv( $csv, "\n" )` does not actually work.
$lines = explode( "\n", rtrim( $csv, "\n" ) );
// @todo When we drop support for PHP <7.4, consider passing empty-string for `$escape` here for better spec compatibility.
return str_getcsv( $line, ',', '"', '\\' );
* Abstract out building the rest api stats path.
* @param string $resource Resource.
function jetpack_stats_api_path( $resource = '' ) {
$resource = ltrim( $resource, '/' );
return sprintf( '/sites/%d/stats/%s', Stats_Options::get_option( 'blog_id' ), $resource );
* Fetches stats data from the REST API. Caches locally for 5 minutes.
* @link: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/stats/
* @deprecated 11.5 Use WPCOM_Stats available methodsinstead.
* @param array $args (default: array()) The args that are passed to the endpoint.
* @param string $resource (default: '') Optional sub-endpoint following /stats/.
function stats_get_from_restapi( $args = array(), $resource = '' ) {
_deprecated_function( __METHOD__, 'jetpack-11.5', 'Please checkout the methods available in Automattic\Jetpack\Stats\WPCOM_Stats' );
$endpoint = jetpack_stats_api_path( $resource );
$args = wp_parse_args( $args, array() );
$cache_key = md5( implode( '|', array( $endpoint, $api_version, wp_json_encode( $args, JSON_UNESCAPED_SLASHES ) ) ) );
$transient_name = "jetpack_restapi_stats_cache_{$cache_key}";
$stats_cache = get_transient( $transient_name );
// Return or expire this key.
$time = key( $stats_cache );
$data = $stats_cache[ $time ]; // WP_Error or string (JSON encoded object).
if ( is_wp_error( $data ) ) {
return (object) array_merge( array( 'cached_at' => $time ), (array) json_decode( $data ) );
$response = Client::wpcom_json_api_request_as_blog( $endpoint, $api_version, $args );
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$data = is_wp_error( $response ) ? $response : new WP_Error( 'stats_error' );
// string (JSON encoded object).
$data = wp_remote_retrieve_body( $response );
// object (rare: null on JSON failure).
$return = json_decode( $data );
// To reduce size in storage: store with time as key, store JSON encoded data (unless error).
set_transient( $transient_name, array( time() => $data ), 5 * MINUTE_IN_SECONDS );
* Add the Jetpack plugin version to the stats tracking data.
* @param array $kvs The stats array in key values.
function filter_stats_array_add_jp_version( $kvs ) {
$kvs['j'] = sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION );
* Convert stats array to object after sanity checking the array is valid.
* @param array $stats_array The stats array.
* @return WP_Error|Object|null
function convert_stats_array_to_object( $stats_array ) {
_deprecated_function( __FUNCTION__, 'jetpack-13.2', 'Automattic\Jetpack\Stats\WPCOM_Stats->convert_stats_array_to_object' );
return ( new WPCOM_Stats() )->convert_stats_array_to_object( $stats_array );