<?php esc_html_e( 'Configure', 'jetpack' ); ?>
<div class="stats-odyssey-notice stats-odyssey-notice--content__highlighted">
<div class="stats-odyssey-notice--content">
<h2 class="stats-odyssey-notice--content-header"><?php esc_html_e( 'Deprecated Jetpack Stats Experience', 'jetpack' ); ?></h2>
<p class="stats-odyssey-notice--content-text"><?php esc_html_e( 'The old Jetpack Stats has been deprecated. Please click the button to enable the new experience.', 'jetpack' ); ?></p>
<div class="stats-odyssey-notice--action-bar">
<button class="dops-button stats-odyssey-notice--primary-button">
<a class="is-primary-link" href="<?php echo esc_url( $redirect_url ); ?>"><?php esc_html_e( 'Switch to new Stats', 'jetpack' ); ?></a>
<a class="is-secondary-link" href="<?php echo esc_url( $learn_url ); ?>" rel="noopener noreferrer" target="_blank"><?php esc_html_e( 'Learn about Stats', 'jetpack' ); ?> <svg xmlns="http://www.w3.org/2000/svg" style="vertical-align: middle;" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"><path d="M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"></path></svg></a>
<div class="stats-odyssey-notice--image-container"></div>
$day = isset( $_GET['day'] ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_GET['day'] ) ? $_GET['day'] : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
'charset' => get_option( 'blog_charset' ),
'color' => get_user_option( 'admin_color' ),
'j' => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
if ( get_locale() !== 'en_US' ) {
$q['jp_lang'] = get_locale();
// Only show the main chart, without extra header data, or metaboxes.
$q['main_chart_only'] = $main_chart_only;
'view' => array( 'referrers', 'postviews', 'searchterms', 'clicks', 'post', 'table' ),
'unit' => array( '1', '7', '31', 'human' ),
'humanize' => array( 'true' ),
'blog_subscribers' => 'int',
'comment_subscribers' => null,
'type' => array( 'wpcom', 'email', 'pending' ),
foreach ( $args as $var => $vals ) {
if ( ! isset( $_REQUEST[ $var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$val = wp_unslash( $_REQUEST[ $var ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( is_array( $vals ) ) {
if ( in_array( $val, $vals, true ) ) {
} elseif ( 'int' === $vals ) {
} elseif ( 'date' === $vals ) {
if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $val ) ) {
} elseif ( null === $vals ) {
} elseif ( 'data' === $vals ) {
if ( str_starts_with( $val, 'index.php' ) ) {
$url = 'https://' . STATS_DASHBOARD_SERVER . '/wp-admin/index.php';
if ( isset( $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( preg_match( '/^[a-z0-9-]+$/', $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
$chart = sanitize_title( $_GET['chart'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
$url = 'https://' . STATS_DASHBOARD_SERVER . "/wp-includes/charts/{$chart}.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 );
} elseif ( ! empty( $get['headers']['content-type'] ) ) {
$type = $get['headers']['content-type'];
if ( str_starts_with( $type, 'image' ) ) {
header( 'Content-Type: ' . $type );
header( 'Content-Length: ' . strlen( $img ) );
echo $img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
if ( isset( $_GET['page'] ) && 'stats' === $_GET['page'] && ! isset( $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$tracking = new Tracking();
$tracking->record_user_event( 'wpa_page_view', array( 'path' => 'old_stats' ) );
if ( isset( $_GET['noheader'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
* Stats Convert Image URLs.
* @param mixed $html HTML.
function stats_convert_image_urls( $html ) {
$url = set_url_scheme( 'https://' . STATS_DASHBOARD_SERVER );
$html = preg_replace( '|(["\'])(/i/stats.+)\\1|', '$1' . $url . '$2$1', $html );
* Callback for preg_replace_callback used in stats_convert_chart_urls()
* @param array $matches The matches resulting from the preg_replace_callback call.
* @return string The admin url for the chart.
function jetpack_stats_convert_chart_urls_callback( $matches ) {
// If there is a query string, change the beginning '?' to a '&' so it fits into the middle of this query string.
return 'admin.php?page=stats&noheader&chart=' . $matches[1] . str_replace( '?', '&', $matches[2] );
* Stats Convert Chart URLs.
* @param mixed $html HTML.
function stats_convert_chart_urls( $html ) {
$html = preg_replace_callback(
'|https?://[-.a-z0-9]+/wp-includes/charts/([-.a-z0-9]+).php(\??)|',
'jetpack_stats_convert_chart_urls_callback',
* Stats Convert Post Title HTML
* @param mixed $html HTML.
function stats_convert_post_titles( $html ) {
$pattern = "<span class='post-(\d+)-link'>.*?</span>";
if ( ! preg_match_all( "!$pattern!", $html, $matches ) ) {
'include' => implode( ',', $matches[1] ),
'suppress_filters' => false,
foreach ( $posts as $post ) {
$stats_posts[ $post->ID ] = $post;
$html = preg_replace_callback( "!$pattern!", 'stats_convert_post_title', $html );
* Stats Convert Post Title Matches.
* @param mixed $matches Matches.
function stats_convert_post_title( $matches ) {
if ( isset( $stats_posts[ $post_id ] ) ) {
return '<a href="' . get_permalink( $post_id ) . '" target="_blank">' . get_the_title( $post_id ) . '</a>';
* CSS to hide the tracking pixel smiley.
* It is now hidden for everyone (used to be visible if you had set the hide_smile option).
function stats_hide_smile_css() {
<style>img#wpstats{display:none}</style>
function stats_admin_bar_head() {
// Let's not show the stats admin bar to users who are not logged in.
if ( ! is_user_logged_in() ) {
if ( ! Stats_Options::get_option( 'admin_bar' ) ) {
if ( ! current_user_can( 'view_stats' ) ) {
if ( ! is_admin_bar_showing() ) {
add_action( 'admin_bar_menu', 'stats_admin_bar_menu', 100 );
<style data-ampdevmode type='text/css'>
#wpadminbar .quicklinks li#wp-admin-bar-stats {
#wpadminbar .quicklinks li#wp-admin-bar-stats a {
#wpadminbar .quicklinks li#wp-admin-bar-stats a div {
#wpadminbar .quicklinks li#wp-admin-bar-stats a:hover div {
#wpadminbar .quicklinks li#wp-admin-bar-stats a img {
* Gets the image source of the given stats chart.
* @param string $chart Name of the chart.
* @param array $args Extra list of argument to use in the image source.
* @return string An image source.
function stats_get_image_chart_src( $chart, $args = array() ) {
$url = add_query_arg( 'page', 'stats', admin_url( 'admin.php' ) );
* @param mixed $wp_admin_bar WPAdminBar.
function stats_admin_bar_menu( &$wp_admin_bar ) {
$img_src = esc_attr( stats_get_image_chart_src( 'admin-bar-hours-scale' ) );
$img_src_2x = esc_attr( stats_get_image_chart_src( 'admin-bar-hours-scale-2x' ) );
$alt = esc_attr( __( 'Stats', 'jetpack' ) );
$title = esc_attr( __( 'Views over 48 hours. Click for more Jetpack Stats.', 'jetpack' ) );
'href' => add_query_arg( 'page', 'stats', admin_url( 'admin.php' ) ), // no menu_page_url() blog-side.
'title' => "<div><img fetchpriority='low' loading='lazy' decoding='async' src='$img_src' srcset='$img_src 1x, $img_src_2x 2x' width='112' height='24' alt='$alt' title='$title'></div>",
$wp_admin_bar->add_menu( $menu );
function stats_get_blog() {
_deprecated_function( __METHOD__, 'jetpack-11.5' );
return Stats_XMLRPC::init()->get_blog();
* Stats Dashboard Widget Options.
* TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
function stats_dashboard_widget_options() {
$options = get_option( 'stats_dashboard_widget' );
if ( ( ! $options ) || ! is_array( $options ) ) {
// Ignore obsolete option values.
$intervals = array( 1, 7, 31, 90, 365 );
foreach ( array( 'top', 'search' ) as $key ) {
if ( isset( $options[ $key ] ) && ! in_array( (int) $options[ $key ], $intervals, true ) ) {
unset( $options[ $key ] );
return array_merge( $defaults, $options );
* Stats Dashboard Widget Control.
* TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
function stats_dashboard_widget_control() {
stats_dashboard_widget_controls_handle_submission();
'1' => __( 'day', 'jetpack' ),
'7' => __( 'week', 'jetpack' ),
'31' => __( 'month', 'jetpack' ),
'1' => __( 'the past day', 'jetpack' ),
'7' => __( 'the past week', 'jetpack' ),
'31' => __( 'the past month', 'jetpack' ),
'90' => __( 'the past quarter', 'jetpack' ),
'365' => __( 'the past year', 'jetpack' ),
stats_dashboard_widget_controls_html( $intervals, $periods, stats_dashboard_widget_options() );
* Handle widget controls form submission.
* TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
function stats_dashboard_widget_controls_handle_submission() {
$options = stats_dashboard_widget_options();
// Check if the correct form was submitted.
if ( isset( $_POST['stats_id'] ) && 'dashboard_stats' === $_POST['stats_id'] ) {
// Perform nonce verification.
isset( $_POST['dashboard-widget-nonce'] ) &&
wp_verify_nonce( filter_var( wp_unslash( $_POST['dashboard-widget-nonce'] ) ), 'edit-dashboard-widget_dashboard_stats' )
$options['chart'] = isset( $_POST['chart'] ) ? (int) $_POST['chart'] : 1;
foreach ( array( 'top', 'search' ) as $key ) {
$options[ $key ] = isset( $_POST[ $key ] ) ? (int) $_POST[ $key ] : $defaults[ $key ];
update_option( 'stats_dashboard_widget', $options );
* Output HTML for widget controls.
* @param array $intervals Array of intervals.
* @param array $periods Array of periods.
* @param array $options Array of options.
* TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
function stats_dashboard_widget_controls_html( $intervals, $periods, $options ) {
<label for="chart"><?php esc_html_e( 'Chart stats by', 'jetpack' ); ?></label>
<select id="chart" name="chart">
foreach ( $periods as $val => $label ) {
<option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['chart'] ); ?>><?php echo esc_html( $label ); ?></option>
<label for="top"><?php esc_html_e( 'Show top posts over', 'jetpack' ); ?></label>
<select id="top" name="top">
foreach ( $intervals as $val => $label ) {
<option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['top'] ); ?>><?php echo esc_html( $label ); ?></option>
<label for="search"><?php esc_html_e( 'Show top search terms over', 'jetpack' ); ?></label>
<select id="search" name="search">
foreach ( $intervals as $val => $label ) {
<option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['search'] ); ?>><?php echo esc_html( $label ); ?></option>
* Jetpack Stats Dashboard Widget.
* TODO: This should be moved into class-jetpack-stats-dashboard-widget.php.
function stats_jetpack_dashboard_widget() {
<form id="stats_dashboard_widget_control" action="<?php echo esc_url( admin_url() ); ?>" method="post">
<?php stats_dashboard_widget_control(); ?>
<?php wp_nonce_field( 'edit-dashboard-widget_dashboard_stats', 'dashboard-widget-nonce' ); ?>
<input type="hidden" name="stats_id" value="dashboard_stats" />
<?php submit_button( __( 'Submit', 'jetpack' ) ); ?>
<button type="button" class="handlediv js-toggle-stats_dashboard_widget_control" aria-expanded="true">
<span class="screen-reader-text"><?php esc_html_e( 'Configure', 'jetpack' ); ?></span>
<span class="toggle-indicator" aria-hidden="true"></span>