This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,471 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 0.31.7 - 2026-06-25
### Changed
- Defer Stats Admin REST controller instantiation so the controller class only loads on REST API requests. [#49803]
## 0.31.6 - 2026-06-15
### Changed
- Internal updates.
## 0.31.5 - 2026-06-08
### Changed
- Internal updates.
## 0.31.4 - 2026-05-25
### Fixed
- Phan: Address `PhanPluginDuplicateConditionalNullCoalescing` violations. [#48887]
## 0.31.3 - 2026-05-19
### Changed
- Internal updates.
## 0.31.2 - 2026-05-04
### Changed
- Internal: No longer require automattic/jetpack-changelogger as a per-project dev dependency. [#48225]
## 0.31.1 - 2026-04-15
### Changed
- Update purchases endpoint from v1.1 /sites/$site/purchases to v1.2 /upgrades?site=$site. [#48035]
## 0.31.0 - 2026-04-10
### Added
- Add GET endpoint for retrieving spam site referrers. [#47566]
## 0.30.9 - 2026-03-23
### Fixed
- Hide stats column for non-public post types in the WP Admin post list. [#47704]
## 0.30.8 - 2026-02-26
### Changed
- Add `AGENTS.md` and `CLAUDE.md` for coding agent guidelines compliance. [#47333]
## 0.30.7 - 2026-02-23
### Added
- Register `STATS_REST_RESP_` transient prefix for cleanup by the stats package transient cleanup cron. [#47213]
## 0.30.6 - 2026-02-02
### Changed
- Update dependencies. [#44736]
## 0.30.5 - 2026-01-26
### Changed
- Update dependencies. [#44736]
## 0.30.4 - 2025-12-15
### Changed
- Internal updates.
## 0.30.3 - 2025-12-08
### Fixed
- Ensure proper flags are used with `json_encode()`. [#46092]
## 0.30.2 - 2025-12-01
### Changed
- Update dependencies. [#44736]
## 0.30.1 - 2025-11-10
### Changed
- Tests: Ensure PHP 8.5 compatibility. [#45769]
### Fixed
- Stats: Display post views after quick edit. [#45793]
## 0.30.0 - 2025-10-27
### Removed
- Remove "Jetpack > Stats" menu. [#45607]
## 0.29.1 - 2025-09-29
### Fixed
- Fix dashboard.wordpress.com breaking non-odyssey stats on older versions of Jetpack. [#45283]
## 0.29.0 - 2025-09-19
### Changed
- Change the location of the Jetpack -> Stats submenu item to a new top-level admin menu item called "Stats". [#44524]
## 0.28.5 - 2025-08-25
### Fixed
- Fix a PHP fatal error in the stats icon column when the first column is "comments". [#44847]
## 0.28.4 - 2025-08-11
### Changed
- Update dependencies. [#44673]
## 0.28.3 - 2025-08-04
### Changed
- Internal updates.
## 0.28.2 - 2025-07-28
### Changed
- Update dependencies. [#39303]
## 0.28.1 - 2025-07-21
### Changed
- Update dependencies. [#39303]
## 0.28.0 - 2025-07-14
### Changed
- Use the `view_stats` capability for the Jetpack Stats menu item instead of `manage_options`. [#44194]
## 0.27.0 - 2025-06-23
### Added
- Add archives endpoint support. [#44028]
## 0.26.3 - 2025-06-02
### Added
- Add query parameters to the stats URL from post list to reconstruct the back URL from stats view. [#43567]
### Fixed
- Fix wrapping on the stats column heading in non-English languages. [#43630]
## 0.26.2 - 2025-05-12
### Fixed
- Stats column: Avoid PHP error when a locale is not available. [#43396]
## 0.26.1 - 2025-05-05
### Changed
- Update dependencies. [#39303]
## 0.26.0 - 2025-04-28
### Removed
- SAL_Site: Remove unused `view_hosting` field. [#43237]
### Fixed
- Stats: Avoid PHP warning when we have no views data about a specific post. [#43146]
## 0.25.2 - 2025-04-21
### Changed
- Internal updates.
## 0.25.1 - 2025-04-14
### Changed
- Internal updates.
## 0.25.0 - 2025-03-31
### Added
- Add page view count in the post and page list. [#42218]
- Update icon in stats column. [#42218]
## 0.24.6 - 2025-03-24
### Changed
- Internal updates.
## 0.24.5 - 2025-03-18
### Changed
- Update dependencies. [#39303]
## 0.24.4 - 2025-03-12
### Changed
- Internal updates.
## 0.24.3 - 2025-03-10
### Changed
- Internal updates.
## 0.24.2 - 2025-02-24
### Changed
- Internal updates.
## 0.24.1 - 2025-02-03
### Changed
- Phan: Update baselines. [#41263]
## 0.24.0 - 2025-01-06
### Added
- Stats: Add API support for location stats. [#40852]
## 0.23.1 - 2024-11-25
### Changed
- Updated dependencies. [#40286]
### Fixed
- Catch an issue when custom code removes `ver` param. [#40322]
- Stats: Remove cache for purchases and usage endpoints. [#40266]
## 0.23.0 - 2024-11-18
### Removed
- General: Update minimum PHP version to 7.2. [#40147]
## 0.22.6 - 2024-11-11
### Fixed
- Fixed flaky tests [#40062]
## 0.22.5 - 2024-11-04
### Added
- Enable test coverage. [#39961]
### Fixed
- API cache not refreshing when cache is bypassed. [#39955]
## 0.22.4 - 2024-10-28
### Changed
- Odyssey Stats cache busting: Use option instead of transient. [#39887]
## 0.22.3 - 2024-10-21
### Changed
- JITM: Expose function to render message. [#39714]
## 0.22.2 - 2024-10-14
### Fixed
- Added missing fields for stats single post endpoint. [#39691]
## 0.22.1 - 2024-09-23
### Changed
- Update dependencies. [#39303]
## 0.22.0 - 2024-09-16
### Added
- Create the user feedback endpoint proxy with a user login status check. [#39287]
## 0.21.2 - 2024-09-09
### Changed
- Update dependencies. [#39253]
## 0.21.1 - 2024-08-23
### Changed
- Updated package dependencies. [#39004]
## 0.21.0 - 2024-07-08
### Added
- Stats: Add purchases endpoint [#38150]
## 0.20.0 - 2024-06-18
### Added
- Check if Jetpack is integrated with the Complianz plugin to show the notice from blocking Stats. [#37870]
## 0.19.3 - 2024-06-10
### Changed
- Update dependencies.
## 0.19.2 - 2024-05-20
### Fixed
- Stats: Use user language instead of site language for translations. [#37423]
## 0.19.1 - 2024-05-06
### Changed
- Internal updates.
## 0.19.0 - 2024-04-29
### Added
- Proxy Devices Stats API endpoint. [#37098]
## 0.18.3 - 2024-04-25
### Changed
- Update dependencies.
## 0.18.2 - 2024-04-22
### Changed
- Internal updates.
## 0.18.1 - 2024-04-08
### Fixed
- Change Odyssey Stats default admin menu name and slug for Simple Classic. [#36689]
## 0.18.0 - 2024-04-01
### Added
- Change Phan baselines. [#36627]
- Stats: Add commercial classification endpoint [#36597]
### Changed
- Add config fields for odyssey stats to be loaded in Simple Site Classic. [#36633]
## 0.17.1 - 2024-03-25
### Changed
- Internal updates.
## 0.17.0 - 2024-03-18
### Added
- Stats: Add UTM stats endpoint [#36402]
## 0.16.2 - 2024-03-04
### Fixed
- Stats: fix posts endpoint for Atomic sites [#36039]
## 0.16.1 - 2024-02-26
### Changed
- Update dependencies.
## 0.16.0 - 2024-02-19
### Added
- Stats: added support for Email stats [#35703]
## 0.15.3 - 2024-02-14
### Fixed
- Stats: clear usage, modules, module-settings cache after purchase [#35590]
## 0.15.2 - 2024-02-05
### Changed
- Update dependencies.
## 0.15.1 - 2023-12-25
### Changed
- Update dependencies.
## 0.15.0 - 2023-12-11
### Added
- Introduced the plan usage API route porting to wpcom. [#34516]
## 0.14.0 - 2023-11-24
### Added
- Added support to load scripts conditionally for the Stats widget. [#34284]
## 0.13.0 - 2023-11-20
### Changed
- Updated required PHP version to >= 7.0. [#34192]
## 0.12.2 - 2023-09-19
### Changed
- Updated Jetpack submenu sort order so individual features are alpha-sorted. [#32958]
## 0.12.1 - 2023-08-28
### Changed
- Updated package dependencies. [#32605]
## 0.12.0 - 2023-07-05
### Added
- Stats Admin: add plan and product for site [#31663]
## 0.11.0 - 2023-06-26
### Added
- Stats Admin: add subscribers endpoints. [#31394]
### Removed
- Stats: removed notices from initial state for better performance. [#31475]
### Fixed
- Stats Admin: fix user capabilities data especially for view_stats. [#31448]
## 0.10.0 - 2023-06-19
### Added
- Add new dependency: Jetpack Plans package. [#31213]
### Changed
- Switch to relying on the Plans package instead of a class available in the Jetpack plugin only. [#31213]
## 0.9.0 - 2023-06-12
### Added
- Stats Admin: added modules toggling API support [#31230]
- Stats Admin: Add module settings API [#31231]
### Changed
- Stats Admin: use WPCOM notices API [#31261]
### Fixed
- Replace dependency on deprecated `automattic/jetpack-options` package. [#31280]
## 0.8.0 - 2023-05-15
### Added
- Stats Admin: adds rest api for marking and unmarking referrers as spam [#30625]
- Stats Admin: adds versions and platform info to Odyssey config data [#30573]
### Fixed
- Stats Admin: align is_automated_transfer with other places [#30622]
## 0.7.3 - 2023-05-01
### Fixed
- Stats Admin: Cache bust the cache buster [#30374]
## 0.7.2 - 2023-04-25
### Changed
- Stats Admin: add minify=false to avoid JS minified by WP.com [#30174]
- Stats Admin: changed to use custom config data variable name to avoid conflicts with `configData` [#30147]
## 0.7.1 - 2023-04-17
### Changed
- Stats Admin: change site locale format from only language code to real locale e.g. `en-us` [#29958]
## 0.7.0 - 2023-04-10
### Added
- Add Jetpack Autoloader package suggestion. [#29988]
- Stats Admin: Adds ability to render Odyssey Widget [#29929]
## 0.6.4 - 2023-04-04
### Changed
- Stats Admin: moved Odyssey assets loading to separate class [#29893]
- Stats Admin: refactoring config data code to Odyssey_Config_Data [#29863]
## 0.6.3 - 2023-03-20
### Added
- Add momentjs dependency for Odyssey Stats [#29329]
## 0.6.2 - 2023-02-28
### Changed
- Stats: moved New Stats toggling logic to stats-admin [#29064]
### Fixed
- Stats Admin: Opt-in notice should be shown a month after the customer has opted out of Odyssey Stats. [#29065]
## 0.6.1 - 2023-02-20
### Fixed
- Fixed unit tests without internet [#28985]
- Stats: fix broken request_as_blog_cached [#28992]
## 0.6.0 - 2023-02-15
### Added
- Stats: Adds support for Notice control [#28857]
### Changed
- Rename stats option enable_calypso_stats to enable_odyssey_stats [#28794]
### Fixed
- Stats: remove unnecessary params that breaks wpcom API [#28935]
## 0.5.0 - 2023-02-08
### Added
- Stats: adds new Stats opt out notice [#28733]
### Changed
- Stats: Remove feature lock for Ads page [#28657]
## 0.4.1 - 2023-01-26
### Fixed
- Stats: fixed missing params to WPCOM API. [#28544]
## 0.4.0 - 2023-01-23
### Added
- Stats: enable Ads page [#27791]
## 0.3.1 - 2023-01-16
### Fixed
- Fix Odyssey Stats footer position [#28308]
## 0.3.0 - 2023-01-11
### Added
- Stats: add loading spinner for Stats Dashboard [#28219]
## 0.2.1 - 2022-12-27
### Changed
- Stats: added more dependencies to be loaded for stats bundle [#28065]
## 0.2.0 - 2022-12-19
### Added
- Stats: added list posts endpoint. [#27875]
### Changed
- Stats: changed loading assets from odyssey-stats folder and some names. [#27971]
- Stats Admin: changed the time to refresh cache buster to 15 min. [#27969]
### Removed
- Stats: removed style overriding for Odyssey stats. [#27896]
### Fixed
- Stats: added `hostname` and `admin_url` to config. [#27922]
- Stats Admin: fixed phpunit CI tests. [#27948]
## 0.1.1 - 2022-12-06
### Changed
- Stats: explicitly allow only certain API access with blog token to wpcom [#27604]
- Updated package dependencies. [#27688]
## 0.1.0 - 2022-11-28
### Added
- Stats: add stats-admin package [#27247]
- Stats: add stats option `enable_calypso_stats` to allow users to enable the new Calypso Stats experience [#27431]
### Changed
- Updated package dependencies. [#27043]
@@ -0,0 +1,363 @@
<?php
/**
* A class that adds a stats column to wp-admin Post List.
*
* @package automattic/jetpack-stats-admin
*/
namespace Automattic\Jetpack\Stats_Admin;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Redirect;
use Automattic\Jetpack\Stats\Options as Stats_Options;
use Automattic\Jetpack\Stats\WPCOM_Stats;
use Automattic\Jetpack\Status\Host;
use NumberFormatter;
/**
* Add a Stats column in the post and page lists.
*/
class Admin_Post_List_Column {
/**
* Create the object.
*
* @return self
*/
public static function register() {
return new self();
}
/**
* A list of NumberFormatters.
*
* @var \NumberFormatter[]
*/
private $formatter;
/**
* The current locale.
*
* @var string
*/
private $locale;
/**
* The constructor.
*/
public function __construct() {
// Add an icon to see stats in WordPress.com for a particular post.
add_action( 'admin_print_styles-edit.php', array( $this, 'stats_load_admin_css' ) );
add_filter( 'manage_posts_columns', array( $this, 'add_stats_post_table' ) );
add_filter( 'manage_pages_columns', array( $this, 'add_stats_post_table' ) );
add_action( 'manage_posts_custom_column', array( $this, 'add_stats_post_table_cell' ), 10, 2 );
add_action( 'manage_pages_custom_column', array( $this, 'add_stats_post_table_cell' ), 10, 2 );
}
/**
* Load CSS needed for Stats column width in WP-Admin area.
*
* @since 4.7.0
*/
public function stats_load_admin_css() {
?>
<style type="text/css">
.fixed .column-stats {
width: 5em;
white-space: nowrap;
}
</style>
<?php
}
/**
* Set content for cell with link to an entry's stats in Odyssey Stats.
*
* @param string $column The name of the column to display.
* @param int $post_id The current post ID.
*
* @since 4.7.0
*/
public function add_stats_post_table_cell( $column, $post_id ) {
if ( 'stats' === $column ) {
if ( 'publish' !== get_post_status( $post_id ) ) {
printf(
'<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>',
esc_html__( 'No stats', 'jetpack-stats-admin' )
);
} else {
// Link to the wp-admin stats page.
$query_args = array(
'from' => 'postList',
'jp_post_type' => get_post_type( $post_id ),
);
$list_criteria_params = array(
's' => sanitize_text_field( get_search_query() ),
'paged' => absint( get_query_var( 'paged' ) ),
'post_status' => sanitize_text_field( get_query_var( 'post_status' ) ),
'orderby' => sanitize_text_field( get_query_var( 'orderby' ) ),
'order' => sanitize_text_field( get_query_var( 'order' ) ),
'author' => absint( get_query_var( 'author' ) ),
'cat' => absint( get_query_var( 'cat' ) ), // 'cat' is the query var for category ID
'm' => absint( get_query_var( 'm' ) ), // 'm' is the query var for YYYYMM
'category_name' => sanitize_text_field( get_query_var( 'category_name' ) ),
);
foreach ( $list_criteria_params as $key => $value ) {
if ( isset( $_GET[ $key ] ) && $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking if the key existts and not reading the value from the request.
$query_args[ 'jp_' . $key ] = $value;
}
}
$stats_post_url = add_query_arg( $query_args, admin_url( 'admin.php?page=stats#!/stats/post/' . $post_id . '/' . \Jetpack_Options::get_option( 'id', 0 ) ) );
// Unless the user is on a Default style WOA site, in which case link to Calypso.
if ( ( new Host() )->is_woa_site() && Stats_Options::get_option( 'enable_odyssey_stats' ) && 'wp-admin' !== get_option( 'wpcom_admin_interface' ) ) {
$stats_post_url = Redirect::get_url(
'calypso-stats-post',
array(
'path' => $post_id,
)
);
}
static $post_views = null;
/**
* Jetpack_stats_get_post_page_views_for_current_list makes a request with all post ids in the current $wp_query.
* This way, we'll make a single API request instead of making one for each post.
*
* For this reason, we'll cache the result with the static $post_views variable.
*/
if ( null === $post_views ) {
$post_views = $this->get_post_page_views_for_current_list();
}
$views = $post_views[ $post_id ] ?? null;
$current_locale = get_locale();
if ( null !== $views ) {
$formatted_views = class_exists( '\NumberFormatter' )
? $this->get_formatter( $current_locale )->format( $views )
: $this->get_fallback_format_to_compact_version( $views );
} else {
$formatted_views = '';
}
?>
<a href="<?php echo esc_url( $stats_post_url ); ?>"
title="<?php echo esc_html__( 'Views for the last thirty days. Click for detailed stats', 'jetpack-stats-admin' ); ?>">
<span
class="dashicons dashicons-visibility"></span>&nbsp;<span><?php echo null !== $views ? esc_html( $formatted_views ) : ''; ?></span>
</a>
<?php
}
}
}
/**
* Set header for column that allows to view an entry's stats.
*
* @param array $columns An array of column names.
*
* @return mixed
*/
public function add_stats_post_table( $columns ) {
// Skip stats column for non-public post types when screen info is available.
if ( function_exists( 'get_current_screen' ) ) {
$screen = get_current_screen();
if ( $screen && $screen->post_type ) {
$post_type_object = get_post_type_object( $screen->post_type );
if ( $post_type_object && ! $post_type_object->public ) {
return $columns;
}
}
}
/**
* The manage_options capability is a fallback for Simple.
* This should be updated with a proper fix. Implemented based on this PR: https://github.com/Automattic/jetpack/pull/41549.
*/
$has_access = current_user_can( 'view_stats' ) || current_user_can( 'manage_options' );
/*
* Stats can be accessed in wp-admin or in Calypso,
* depending on what version of the stats screen is enabled on your site.
*
* In both cases, the user must be allowed to access stats.
*
* If the Odyssey Stats experience isn't enabled, the user will need to go to Calypso,
* so they need to be connected to WordPress.com to be able to access that page.
*/
if (
! $has_access
|| (
! Stats_Options::get_option( 'enable_odyssey_stats' )
&& ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected()
)
) {
return $columns;
}
// Array-Fu to add before comments.
$pos = array_search( 'comments', array_keys( $columns ), true );
// Fallback to the last position if the post type does not support comments.
if ( ! is_int( $pos ) ) {
$pos = count( $columns );
}
// If comments position is 0, then prepend the element at the beginning of the array.
if ( 0 === $pos ) {
return array_merge(
array( 'stats' => esc_html__( 'Stats', 'jetpack-stats-admin' ) ),
$columns
);
}
$chunks = array_chunk( $columns, $pos, true );
$chunks[0]['stats'] = esc_html__( 'Stats', 'jetpack-stats-admin' );
return call_user_func_array( 'array_merge', $chunks );
}
/**
* Get a list of post views for each post id from the global $wp_query.
*
* @return array
*/
public function get_post_page_views_for_current_list(): array {
global $wp_query;
if ( $wp_query->posts ) {
$post_ids = wp_list_pluck( $wp_query->posts, 'ID' );
} elseif ( wp_doing_ajax() && ! empty( $_POST['action'] ) && 'inline-save' === $_POST['action'] && ! empty( $_POST['post_ID'] ) && check_ajax_referer( 'inlineeditnonce', '_inline_edit' ) ) {
$post_ids = array( sanitize_text_field( wp_unslash( $_POST['post_ID'] ) ) );
} else {
return array();
}
$wpcom_stats = $this->get_stats();
$post_views = $wpcom_stats->get_total_post_views(
array(
'num' => 30,
'post_ids' => implode( ',', $post_ids ),
)
);
if ( is_wp_error( $post_views ) || empty( $post_views ) ) {
return array();
}
$views = array();
foreach ( $post_views['posts'] as $post ) {
$views[ $post['ID'] ] = $post['views'];
}
return $views;
}
/**
* Get the stats object.
*
* @return WPCOM_Stats
*/
protected function get_stats() {
return new WPCOM_Stats();
}
/**
* Get and validate the locale.
*
* @param string $locale The locale to validate.
*
* @return string The validated locale.
*/
public function get_validated_locale( string $locale ): string {
if ( isset( $this->locale ) ) {
return $this->locale;
}
/*
* Check if the locale is valid and available.
* If not, fallback to en_US.
*/
if ( ! in_array( $locale, \IntlCalendar::getAvailableLocales(), true ) ) {
$locale = 'en_US';
}
$this->locale = $locale;
return $locale;
}
/**
* Get the NumberFormatter instance.
*
* @param string $locale The current locale.
*
* @return NumberFormatter
*/
protected function get_formatter( string $locale ): \NumberFormatter {
if ( isset( $this->formatter[ $locale ] ) ) {
return $this->formatter[ $locale ];
}
$locale = $this->get_validated_locale( $locale );
/**
* 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).
*
* @see https://unicode-org.github.io/icu-docs/apidoc/dev/icu4c/unum_8h.html UNUM_DECIMAL_COMPACT_SHORT constant.
*/
$compact_decimal_short = 14;
/**
* 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.
*
* Use the constant if it's defined since it's safer.
*/
if ( defined( '\NumberFormatter::DECIMAL_COMPACT_SHORT' ) ) {
// @phan-suppress-next-line PhanUndeclaredConstantOfClass
$compact_decimal_short = NumberFormatter::DECIMAL_COMPACT_SHORT;
}
try {
$formatter = new \NumberFormatter( $locale, $compact_decimal_short );
$formatter->setAttribute( \NumberFormatter::MAX_FRACTION_DIGITS, 1 );
} catch ( \Exception $e ) {
// Fallback to decimal if for some reason it fails to work.
$formatter = new \NumberFormatter( $locale, \NumberFormatter::DECIMAL );
}
$this->formatter[ $locale ] = $formatter;
return $formatter;
}
/**
* Fallback Format a number to a compact version if the Intl extension is not available.
*
* @param int $views The given number.
*
* @return string
*/
public function get_fallback_format_to_compact_version( $views ) {
if ( $views >= 10000000 ) {
return round( $views / 1000000 ) . 'M';
} elseif ( $views >= 1000000 ) {
$views = round( $views / 1000000, 1 );
return preg_replace( '/\.0$/', '', (string) $views ) . 'M';
} elseif ( $views >= 10000 ) {
return round( $views / 1000 ) . 'K';
} elseif ( $views >= 1000 ) {
$views = round( $views / 1000, 1 );
return preg_replace( '/\.0$/', '', (string) $views ) . 'K';
}
return (string) $views;
}
}
@@ -0,0 +1,142 @@
<?php
/**
* A class that adds a stats dashboard to wp-admin.
*
* @package automattic/jetpack-stats-admin
*/
namespace Automattic\Jetpack\Stats_Admin;
use Automattic\Jetpack\Stats\Options as Stats_Options;
/**
* Responsible for adding a stats dashboard to wp-admin.
*
* @package jetpack-stats-admin
*/
class Dashboard {
/**
* Whether the class has been initialized
*
* @var boolean
*/
private static $initialized = false;
/**
* Priority for the dashboard menu
* For Jetpack sites: Jetpack uses 998 and 'Admin_Menu' uses 1000, so we need to use 999.
* For simple site: the value is overriden in a child class with value 100000 to wait for all menus to be registered.
*
* @var int
*/
protected $menu_priority = 999;
/**
* Init Stats dashboard.
*/
public static function init() {
if ( ! self::$initialized ) {
self::$initialized = true;
( new self() )->init_hooks();
}
}
/**
* Initialize the hooks.
*/
public function init_hooks() {
self::$initialized = true;
// Jetpack uses 998 and 'Admin_Menu' uses 1000.
add_action( 'admin_menu', array( $this, 'add_wp_admin_menu' ), $this->menu_priority );
}
/**
* Add a "Stats" top-level admin menu.
*
* @return void
*/
public function add_wp_admin_menu() {
/**
* Disable this menu for dashboard.wordpress.com because older versions of Jetpack need to fetch the old Stats UI.
*
* If this menu is registered, it will conflict with the back-end and break non-odyssey Stats.
*/
if ( defined( 'IS_WPCOM' ) && IS_WPCOM && 120742 === get_current_blog_id() ) {
return;
}
$page_suffix = add_menu_page(
__( 'Stats', 'jetpack-stats-admin' ),
_x( 'Stats', 'product name shown in menu', 'jetpack-stats-admin' ),
'view_stats',
'stats',
array( $this, 'render' ),
'dashicons-chart-bar',
2
);
if ( $page_suffix ) {
add_action( 'load-' . $page_suffix, array( $this, 'admin_init' ) );
}
}
/**
* Override render funtion
*/
public function render() {
// Record the number of views of the stats dashboard on the initial several loads for the purpose of showing feedback notice.
$views = intval( Stats_Options::get_option( 'views' ) ) + 1;
if ( $views <= Notices::VIEWS_TO_SHOW_FEEDBACK ) {
Stats_Options::set_option( 'views', $views );
}
?>
<div id="wpcom" class="jp-stats-dashboard" style="min-height: calc(100vh - 100px);">
<div class="hide-if-js"><?php esc_html_e( 'Your Jetpack Stats dashboard requires JavaScript to function properly.', 'jetpack-stats-admin' ); ?></div>
<div class="hide-if-no-js" style="height: 100%">
<img
class="jp-stats-dashboard-loading-spinner"
width="32"
height="32"
style="position: absolute; left: 50%; top: 50%;"
alt=<?php echo esc_attr( __( 'Loading', 'jetpack-stats-admin' ) ); ?>
src="//en.wordpress.com/i/loading/loading-64.gif"
/>
</div>
</div>
<script>
jQuery(document).ready(function($) {
// Load SVG sprite.
$.get("https://widgets.wp.com/odyssey-stats/common/gridicons-506499ddac13811fee8e.svg", function(data) {
var div = document.createElement("div");
div.innerHTML = new XMLSerializer().serializeToString(data.documentElement);
div.style = 'display: none';
document.body.insertBefore(div, document.body.childNodes[0]);
});
// we intercept on all anchor tags and change it to hashbang style.
$("#wpcom").on('click', 'a', function (e) {
const link = e && e.currentTarget && e.currentTarget.attributes && e.currentTarget.attributes.href && e.currentTarget.attributes.href.value;
if( link && link.startsWith( '/stats' ) ) {
location.hash = `#!${link}`;
return false;
}
});
});
</script>
<?php
}
/**
* Initialize the admin resources.
*/
public function admin_init() {
add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_scripts' ) );
}
/**
* Load the admin scripts.
*/
public function load_admin_scripts() {
( new Odyssey_Assets() )->load_admin_scripts( 'jp-stats-dashboard', 'build.min', array( 'config_variable_name' => 'jetpackStatsOdysseyAppConfigData' ) );
}
}
@@ -0,0 +1,110 @@
<?php
/**
* Stats Main
*
* @package automattic/jetpack-stats-admin
*/
namespace Automattic\Jetpack\Stats_Admin;
use Automattic\Jetpack\Connection\Manager;
use Automattic\Jetpack\Stats\Options as Stats_Options;
use Automattic\Jetpack\Tracking;
/**
* Stats Main class.
*
* Entrypoint for Stats.
*
* @since 0.1.0
*/
class Main {
/**
* Stats version.
*/
const VERSION = '0.31.7';
/**
* Singleton Main instance.
*
* @var Main
**/
private static $instance = null;
/**
* Initializer.
* Used to configure the stats package, eg when called via the Config package.
*
* @return object
*/
public static function init() {
if ( null === self::$instance ) {
self::$instance = new Main();
}
return self::$instance;
}
/**
* Class constructor.
*
* @return void
*/
private function __construct() {
add_action( 'rest_api_init', array( REST_Controller::class, 'register' ) );
// Disable JITM assets on the Stats page.
// JITM is handled separately by Stats: https://github.com/Automattic/wp-calypso/pull/95273.
add_filter(
'jetpack_display_jitms_on_screen',
function ( $show, $screen_id ) {
if ( 'jetpack_page_stats' === $screen_id ) {
return false;
}
return $show;
},
10,
2
);
// Register stats-admin transient prefix for cleanup by the stats package.
add_filter( 'jetpack_stats_transient_cleanup_prefixes', array( $this, 'register_transient_cleanup_prefix' ) );
}
/**
* Register the stats-admin transient prefix for cleanup.
*
* @param array $prefixes List of transient prefixes to clean up.
* @return array Modified list of prefixes.
*/
public function register_transient_cleanup_prefix( $prefixes ) {
$prefixes[] = WPCOM_Client::CACHE_TRANSIENT_PREFIX;
return $prefixes;
}
/**
* Update New Stats status.
*
* @param bool $status true to enable or false to disable.
* @return bool
*/
public static function update_new_stats_status( $status ) {
$status = (bool) $status;
$stats_options = array(
'enable_odyssey_stats' => $status,
'odyssey_stats_changed_at' => time(),
);
$updated = Stats_Options::set_options( $stats_options );
// Track the event.
$event_name = 'calypso_stats_disabled';
if ( $status ) {
$event_name = 'calypso_stats_enabled';
}
$connection_manager = new Manager( 'jetpack' );
$tracking = new Tracking( 'jetpack', $connection_manager );
$tracking->record_user_event( $event_name, array_merge( $stats_options, array( 'updated' => $updated ) ) );
return $updated;
}
}
@@ -0,0 +1,139 @@
<?php
/**
* A class that handles the notices for the Stats Admin dashboard.
*
* @package automattic/jetpack-stats-admin
*/
namespace Automattic\Jetpack\Stats_Admin;
use Automattic\Jetpack\Stats\Options as Stats_Options;
use Jetpack_Options;
/**
* The Notices class handles the notices for the Stats Admin dashboard.
*
* @package Automattic\Jetpack\Stats_Admin
*/
class Notices {
const STATS_DASHBOARD_NOTICES_CACHE_KEY = 'jetpack_stats_dashboard_notices_cache_key';
const OPT_OUT_NEW_STATS_NOTICE_ID = 'opt_out_new_stats';
const NEW_STATS_FEEDBACK_NOTICE_ID = 'new_stats_feedback';
const OPT_IN_NEW_STATS_NOTICE_ID = 'opt_in_new_stats';
const GDPR_COOKIE_CONSENT_NOTICE_ID = 'gdpr_cookie_consent';
const VIEWS_TO_SHOW_FEEDBACK = 3;
const POSTPONE_OPT_IN_NOTICE_DAYS = 30;
/**
* Update notice status.
*
* @param mixed $id ID of the notice.
* @param mixed $status Status of the notice.
* @param int $postponed_for Postponed for how many seconds.
* @return bool
*/
public function update_notice( $id, $status, $postponed_for = 0 ) {
delete_transient( self::STATS_DASHBOARD_NOTICES_CACHE_KEY );
return WPCOM_Client::request_as_blog(
sprintf(
'/sites/%d/jetpack-stats-dashboard/notices',
Jetpack_Options::get_option( 'id' )
),
'v2',
array(
'timeout' => 5,
'method' => 'POST',
'headers' => array(
'Content-Type' => 'application/json',
),
),
wp_json_encode(
array(
'id' => $id,
'status' => $status,
'postponed_for' => $postponed_for,
),
JSON_UNESCAPED_SLASHES
),
'wpcom'
);
}
/**
* Return an array of notices IDs as keys and their value to flag whther to show them.
*
* @return array
*/
public function get_notices_to_show() {
$notices_wpcom = $this->get_notices_from_wpcom();
$new_stats_enabled = Stats_Options::get_option( 'enable_odyssey_stats' );
$stats_views = intval( Stats_Options::get_option( 'views' ) );
$odyssey_stats_changed_at = intval( Stats_Options::get_option( 'odyssey_stats_changed_at' ) );
// Check if Jetpack is integrated with the Complianz plugin, which blocks the Stats.
$complianz_options_integrations = get_option( 'complianz_options_integrations' );
$is_jetpack_blocked_by_complianz = ! isset( $complianz_options_integrations['jetpack'] ) || $complianz_options_integrations['jetpack'];
return array_merge(
$notices_wpcom,
array(
// Show Opt-in notice 30 days after the new stats being disabled.
self::OPT_IN_NEW_STATS_NOTICE_ID => ! $new_stats_enabled
&& $odyssey_stats_changed_at < time() - self::POSTPONE_OPT_IN_NOTICE_DAYS * DAY_IN_SECONDS
&& ! $this->is_notice_hidden( self::OPT_IN_NEW_STATS_NOTICE_ID ),
// Show feedback notice after 3 views of the new stats.
self::NEW_STATS_FEEDBACK_NOTICE_ID => $new_stats_enabled
&& $stats_views >= self::VIEWS_TO_SHOW_FEEDBACK
&& ! $this->is_notice_hidden( self::NEW_STATS_FEEDBACK_NOTICE_ID ),
// Show opt-out notice before 3 views of the new stats, where 3 is included.
self::OPT_OUT_NEW_STATS_NOTICE_ID => $new_stats_enabled
&& $stats_views < self::VIEWS_TO_SHOW_FEEDBACK
&& ! $this->is_notice_hidden( self::OPT_OUT_NEW_STATS_NOTICE_ID ),
// GDPR cookie consent notice for Complianz users.
self::GDPR_COOKIE_CONSENT_NOTICE_ID => class_exists( 'COMPLIANZ' ) && $is_jetpack_blocked_by_complianz
&& ! $this->is_notice_hidden( self::GDPR_COOKIE_CONSENT_NOTICE_ID ),
)
);
}
/**
* Get the array of hidden notices from WPCOM.
*/
public function get_notices_from_wpcom() {
$notices_wpcom = WPCOM_Client::request_as_blog_cached(
sprintf(
'/sites/%d/jetpack-stats-dashboard/notices',
Jetpack_Options::get_option( 'id' )
),
'v2',
array(
'timeout' => 5,
),
null,
'wpcom',
true,
static::STATS_DASHBOARD_NOTICES_CACHE_KEY
);
if ( is_wp_error( $notices_wpcom ) ) {
return array();
}
return $notices_wpcom;
}
/**
* Checks if a notice is hidden.
*
* @param mixed $id ID of the notice.
* @return bool
*/
public function is_notice_hidden( $id ) {
$notices_wpcom = $this->get_notices_from_wpcom();
return array_key_exists( $id, $notices_wpcom ) && $notices_wpcom[ $id ] === false;
}
}
@@ -0,0 +1,145 @@
<?php
/**
* Stats Assets
*
* @package automattic/jetpack-stats-admin
*/
namespace Automattic\Jetpack\Stats_Admin;
use Automattic\Jetpack\Assets;
/**
* Class Odyssey_Config_Data
*
* @package automattic/jetpack-stats-admin
*/
class Odyssey_Assets {
// This is a fixed list @see https://github.com/Automattic/wp-calypso/pull/71442/
const JS_DEPENDENCIES = array( 'lodash', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-polyfill', 'wp-primitives', 'wp-url', 'wp-warning', 'moment' );
// Sometimes custom scripts would strip the `ver` query params, so we need to make sure it doesn't by adding a custom version param `osv` here.
const ODYSSEY_CDN_URL = 'https://widgets.wp.com/odyssey-stats/%s/%s?minify=false&osv=%s';
/**
* We bump the asset version when the Jetpack back end is not compatible anymore.
*/
const ODYSSEY_STATS_VERSION = 'v1';
const ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY = 'odyssey_stats_admin_asset_cache_buster';
/**
* Load the admin scripts.
*
* @param string $asset_handle The handle of the asset.
* @param string $asset_name The name of the asset.
* @param array $options The options.
*/
public function load_admin_scripts( $asset_handle, $asset_name, $options = array() ) {
$default_options = array(
'config_data' => ( new Odyssey_Config_Data() )->get_data(),
'config_variable_name' => 'configData',
'enqueue_css' => true,
);
$options = wp_parse_args( $options, $default_options );
if ( file_exists( __DIR__ . "/../dist/{$asset_name}.js" ) ) {
// Load local assets for the convinience of development.
Assets::register_script(
$asset_handle,
"../dist/{$asset_name}.js",
__FILE__,
array(
'in_footer' => true,
'textdomain' => 'jetpack-stats-admin',
)
);
Assets::enqueue_script( $asset_handle );
} else {
// In production, we load the assets from our CDN.
wp_register_script(
$asset_handle,
sprintf( self::ODYSSEY_CDN_URL, self::ODYSSEY_STATS_VERSION, "{$asset_name}.js", $this->get_cdn_asset_cache_buster() ),
self::JS_DEPENDENCIES,
$this->get_cdn_asset_cache_buster(),
true
);
wp_enqueue_script( $asset_handle );
// Enqueue CSS if needed.
if ( $options['enqueue_css'] ) {
$css_url = $asset_name . ( is_rtl() ? '.rtl' : '' ) . '.css';
$css_handle = $asset_handle . '-style';
wp_register_style(
$css_handle,
sprintf( self::ODYSSEY_CDN_URL, self::ODYSSEY_STATS_VERSION, $css_url, $this->get_cdn_asset_cache_buster() ),
array(),
$this->get_cdn_asset_cache_buster()
);
wp_enqueue_style( $css_handle );
}
}
wp_add_inline_script(
$asset_handle,
( new Odyssey_Config_Data() )->get_js_config_data( $options['config_variable_name'], $options['config_data'] ),
'before'
);
}
/**
* Returns cache buster string for assets.
* Development mode doesn't need this, as it's handled by `Assets` class.
*
* @return string
*/
protected function get_cdn_asset_cache_buster() {
$now_in_ms = floor( microtime( true ) * 1000 );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['force_refresh'] ) ) {
update_option( self::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY, $this->get_cache_buster_option_value( $now_in_ms ), false );
}
// Use cached cache buster in production.
$remote_asset_version = get_option( self::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY );
if ( ! empty( $remote_asset_version ) ) {
$remote_asset_version = json_decode( $remote_asset_version, true );
// If cache buster is cached and not expired (valid in 15 min), return it.
if ( ! empty( $remote_asset_version['cache_buster'] ) && $remote_asset_version['cached_at'] > $now_in_ms - MINUTE_IN_SECONDS * 1000 * 15 ) {
return $remote_asset_version['cache_buster'];
}
}
// If no cached cache buster, we fetch it from CDN and set to transient.
$response = wp_remote_get( sprintf( self::ODYSSEY_CDN_URL, self::ODYSSEY_STATS_VERSION, 'build_meta.json', $now_in_ms ), array( 'timeout' => 5 ) );
if ( is_wp_error( $response ) ) {
// fallback to current timestamp.
return (string) $now_in_ms;
}
$build_meta = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! empty( $build_meta['cache_buster'] ) ) {
// Cache the cache buster for 15 mins.
update_option( self::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY, $this->get_cache_buster_option_value( $build_meta['cache_buster'] ), false );
return $build_meta['cache_buster'];
}
// fallback to current timestamp.
return (string) $now_in_ms;
}
/**
* Get the cache buster option value.
*
* @param string|int|float $cache_buster The cache buster.
* @return string|false
*/
protected function get_cache_buster_option_value( $cache_buster ) {
return wp_json_encode(
array(
'cache_buster' => (string) $cache_buster,
'cached_at' => floor( microtime( true ) * 1000 ), // milliseconds.
),
JSON_UNESCAPED_SLASHES
);
}
}
@@ -0,0 +1,230 @@
<?php
/**
* Stats Initial State
*
* @package automattic/jetpack-stats-admin
*/
namespace Automattic\Jetpack\Stats_Admin;
use Automattic\Jetpack\Blaze;
use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Status\Host;
use Jetpack_Options;
/**
* Class Odyssey_Config_Data
*
* @package automattic/jetpack-stats-admin
*/
class Odyssey_Config_Data {
/**
* Set configData to window.configData.
*
* @param string $config_variable_name The config variable name.
* @param array $config_data The config data.
*/
public function get_js_config_data( $config_variable_name = 'configData', $config_data = null ) {
return "window.{$config_variable_name} = " . wp_json_encode(
$config_data === null ? $this->get_data() : $config_data,
JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
) . ';';
}
/**
* Return the config for the app.
*/
public function get_data() {
global $wp_version;
$blog_id = Jetpack_Options::get_option( 'id' );
$host = new Host();
$can_blaze = class_exists( 'Automattic\Jetpack\Blaze' ) && Blaze::should_initialize()['can_init'];
return array(
'admin_page_base' => $this->get_admin_path(),
'api_root' => esc_url_raw( rest_url() ),
'blog_id' => Jetpack_Options::get_option( 'id' ),
'enable_all_sections' => false,
'env_id' => 'production',
'google_analytics_key' => 'UA-10673494-15',
'google_maps_and_places_api_key' => '',
'hostname' => wp_parse_url( get_site_url(), PHP_URL_HOST ),
'i18n_default_locale_slug' => 'en',
'i18n_locale_slug' => $this->get_user_locale(),
'mc_analytics_enabled' => false,
'meta' => array(),
'nonce' => wp_create_nonce( 'wp_rest' ),
'site_name' => \get_bloginfo( 'name' ),
'sections' => array(),
// Features are inlined @see https://github.com/Automattic/wp-calypso/pull/70122
'features' => array(
'is_running_in_jetpack_site' => ! $host->is_wpcom_simple(),
),
// Intended for apps that do not use redux.
'gmt_offset' => $this->get_gmt_offset(),
'odyssey_stats_base_url' => admin_url( 'admin.php?page=stats' ),
'intial_state' => array(
'currentUser' => array(
'id' => 1000,
'user' => array(
'ID' => 1000,
'username' => 'no-user',
),
'capabilities' => array(
"$blog_id" => $this->get_current_user_capabilities(),
),
),
'sites' => array(
'items' => array(
"$blog_id" => array(
'ID' => $blog_id,
'URL' => site_url(),
// Atomic and jetpack sites should return true.
'jetpack' => ! $host->is_wpcom_simple(),
'visible' => true,
'capabilities' => $this->get_current_user_capabilities(),
'products' => Jetpack_Plan::get_products(),
'plan' => $this->get_plan(),
'options' => array(
'wordads' => ( new Modules() )->is_active( 'wordads' ),
'admin_url' => admin_url(),
'gmt_offset' => $this->get_gmt_offset(),
'is_automated_transfer' => $this->is_automated_transfer( $blog_id ),
'is_wpcom_atomic' => $host->is_woa_site(),
'is_wpcom_simple' => $host->is_wpcom_simple(),
'is_vip' => $host->is_vip_site(),
'jetpack_version' => defined( 'JETPACK__VERSION' ) ? JETPACK__VERSION : '',
'stats_admin_version' => Main::VERSION,
'software_version' => $wp_version,
'can_blaze' => $can_blaze,
),
),
),
'features' => array( "$blog_id" => array( 'data' => $this->get_plan_features() ) ),
),
),
);
}
/**
* Defines a filter to set whether a site is an automated_transfer site or not.
*
* Default is false. On Atomic, this is set to true by `wpcomsh`.
*
* @param int $blog_id Blog ID.
*
* @return bool
*/
public function is_automated_transfer( $blog_id ) {
/**
* Filter if a site is an automated-transfer site.
*
* @module json-api
*
* @since 6.4.0
*
* @param bool is_automated_transfer( $this->blog_id )
* @param int $blog_id Blog identifier.
*/
return apply_filters(
'jetpack_site_automated_transfer',
false,
$blog_id
);
}
/**
* Get the current site GMT Offset.
*
* @return float The current site GMT Offset by hours.
*/
protected function get_gmt_offset() {
return (float) get_option( 'gmt_offset' );
}
/**
* Page base for the Calypso admin page.
*/
protected function get_admin_path() {
if ( ! isset( $_SERVER['PHP_SELF'] ) || ! isset( $_SERVER['QUERY_STRING'] ) ) {
$parsed = wp_parse_url( admin_url( 'admin.php?page=stats' ) );
return $parsed['path'] . '?' . $parsed['query'];
}
// We do this because page.js requires the exactly page base to be set otherwise it will not work properly.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
return wp_unslash( $_SERVER['PHP_SELF'] ) . '?' . wp_unslash( $_SERVER['QUERY_STRING'] );
}
/**
* Get locale acceptable by Calypso.
*/
protected function get_user_locale() {
/**
* In WP, locales are formatted as LANGUAGE_REGION, for example `en`, `en_US`, `es_AR`,
* but Calypso expects language-region, e.g. `en-us`, `en`, `es-ar`. So we need to convert
* them to lower case and replace the underscore with a dash.
*/
$locale = strtolower( get_user_locale() );
$locale = str_replace( '_', '-', $locale );
return $locale;
}
/**
* Get the features of the current plan.
*/
protected function get_plan_features() {
$plan = Jetpack_Plan::get();
if ( empty( $plan['features'] ) ) {
return array();
}
return $plan['features'];
}
/**
* Get the current plan.
*
* @return array
*/
protected function get_plan() {
$plan = Jetpack_Plan::get();
unset( $plan['features'] );
unset( $plan['supports'] );
return $plan;
}
/**
* Get the capabilities of the current user.
*
* @return array An array of capabilities.
*/
protected function get_current_user_capabilities() {
return array(
'edit_pages' => current_user_can( 'edit_pages' ),
'edit_posts' => current_user_can( 'edit_posts' ),
'edit_others_posts' => current_user_can( 'edit_others_posts' ),
'edit_others_pages' => current_user_can( 'edit_others_pages' ),
'delete_posts' => current_user_can( 'delete_posts' ),
'delete_others_posts' => current_user_can( 'delete_others_posts' ),
'edit_theme_options' => current_user_can( 'edit_theme_options' ),
'edit_users' => current_user_can( 'edit_users' ),
'list_users' => current_user_can( 'list_users' ),
'manage_categories' => current_user_can( 'manage_categories' ),
'manage_options' => current_user_can( 'manage_options' ),
'moderate_comments' => current_user_can( 'moderate_comments' ),
'activate_wordads' => current_user_can( 'manage_options' ),
'promote_users' => current_user_can( 'promote_users' ),
'publish_posts' => current_user_can( 'publish_posts' ),
'upload_files' => current_user_can( 'upload_files' ),
'delete_users' => current_user_can( 'delete_users' ),
'remove_users' => current_user_can( 'remove_users' ),
'own_site' => current_user_can( 'manage_options' ), // Administrators are considered owners on site.
'view_stats' => current_user_can( 'view_stats' ),
'activate_plugins' => current_user_can( 'activate_plugins' ),
);
}
}
@@ -0,0 +1,90 @@
<?php
/**
* A class that adds Jetpack Stats widget for WP Dashboard.
*
* @package automattic/jetpack-stats-admin
*/
namespace Automattic\Jetpack\Stats_Admin;
/**
* Responsible for loading Jetpack Stats widget for WP Dashboard.
*
* @package jetpack-stats-admin
*/
class WP_Dashboard_Odyssey_Widget {
const DASHBOARD_WIDGET_ID = 'jetpack_summary_widget';
/**
* Renders the widget and fires a dashboard widget action.
*/
public function render() {
// The widget is always rendered, so if it was hidden and then toggled open, we need to ask user to refresh the page to load data properly.
$is_toggled_open = $this->is_widget_hidden();
?>
<div id="dashboard_stats" class="jp-stats-widget" style="min-height: 200px;">
<div class="hide-if-js"><?php esc_html_e( 'Your Jetpack Stats widget requires JavaScript to function properly.', 'jetpack-stats-admin' ); ?></div>
<div class="hide-if-no-js" style="height: 100%">
<img
class="jp-stats-widget-loading-spinner"
width="32"
height="32"
style="position: absolute; left: calc(50% - 28px); top: calc(50% - 36px);"
alt=<?php echo esc_attr( __( 'Loading', 'jetpack-stats-admin' ) ); ?>
src="//en.wordpress.com/i/loading/loading-64.gif"
/>
<p>
<?php
if ( $is_toggled_open ) {
echo esc_html__( 'Please reload the page to see your stats...', 'jetpack-stats-admin' );
}
?>
</p>
</div>
</div>
<?php
/**
* Fires when the dashboard is loaded, but no longer used anywhere in the Jetpack plugin.
* The action is still available for backward compatibility.
*
* @since 3.4.0
*/
do_action( 'jetpack_dashboard_widget' );
}
/**
* Load the admin scripts.
*/
public function load_admin_scripts() {
$config_data = ( new Odyssey_Config_Data() )->get_data();
( new Odyssey_Assets() )->load_admin_scripts(
'jetpack_stats_widget',
'widget-loader.min',
array(
'config_variable_name' => 'jetpackStatsOdysseyWidgetConfigData',
'config_data' => $config_data,
'enqueue_css' => false,
)
);
}
/**
* Load the admin scripts when the widget is visible.
*/
public function maybe_load_admin_scripts() {
if ( $this->is_widget_hidden() ) {
return;
}
$this->load_admin_scripts();
}
/**
* Returns true if the widget is hidden for the current screen and current user.
*
* @return bool
*/
public function is_widget_hidden() {
$hidden = get_hidden_meta_boxes( get_current_screen() );
return in_array( self::DASHBOARD_WIDGET_ID, $hidden, true );
}
}
@@ -0,0 +1,141 @@
<?php
/**
* A class that wraps `Automattic\Jetpack\Connection\Client` and handles cache and errors.
*
* @package automattic/jetpack-stats-admin
*/
namespace Automattic\Jetpack\Stats_Admin;
use Automattic\Jetpack\Connection\Client;
use WP_Error;
/**
* A class that wraps `Automattic\Jetpack\Connection\Client` and handles cache and errors.
*
* @package Automattic\Jetpack\Stats_Admin
*/
class WPCOM_Client {
/**
* Transient prefix for caching REST API responses.
*
* @var string
*/
const CACHE_TRANSIENT_PREFIX = 'STATS_REST_RESP_';
/**
* Query the WordPress.com REST API using the blog token cached.
*
* @param String $path The API endpoint relative path.
* @param String $version The API version.
* @param array $args Request arguments.
* @param String $body Request body.
* @param String $base_api_path (optional) the API base path override, defaults to 'rest'.
* @param bool $use_cache (optional) default to true.
* @param string $cache_key (optional) default to null meaning the function auto generates cache key.
* @return array|WP_Error $response Data.
*/
public static function request_as_blog_cached( $path, $version = '1.1', $args = array(), $body = null, $base_api_path = 'rest', $use_cache = true, $cache_key = null ) {
// Only allow caching GET requests.
$use_cache = $use_cache && ! ( isset( $args['method'] ) && strtoupper( $args['method'] ) !== 'GET' ) && ! static::should_bypass_cache();
// Arrays are serialized without considering the order of objects, but it's okay atm.
$cache_key = $cache_key ?? self::CACHE_TRANSIENT_PREFIX . md5( implode( '|', array( $path, $version, wp_json_encode( $args, JSON_UNESCAPED_SLASHES ), wp_json_encode( $body, JSON_UNESCAPED_SLASHES ), $base_api_path ) ) );
if ( $use_cache ) {
$response_body_content = get_transient( $cache_key );
if ( false !== $response_body_content ) {
return json_decode( $response_body_content, true );
}
}
$response_body = static::request_as_blog( $path, $version, $args, $body, $base_api_path );
if ( is_wp_error( $response_body ) ) {
return $response_body;
}
// Cache the response for 5 minutes.
set_transient( $cache_key, wp_json_encode( $response_body, JSON_UNESCAPED_SLASHES ), 5 * MINUTE_IN_SECONDS );
return $response_body;
}
/**
* Query the WordPress.com REST API using the blog token
*
* @param String $path The API endpoint relative path.
* @param String $version The API version.
* @param array $args Request arguments.
* @param String $body Request body.
* @param String $base_api_path (optional) the API base path override, defaults to 'rest'.
* @return array|WP_Error $response Data.
*/
public static function request_as_blog( $path, $version = '1.1', $args = array(), $body = null, $base_api_path = 'rest' ) {
$response = Client::wpcom_json_api_request_as_blog(
$path,
$version,
$args,
$body,
$base_api_path
);
if ( is_wp_error( $response ) ) {
return $response;
}
$response_code = wp_remote_retrieve_response_code( $response );
$response_body_content = wp_remote_retrieve_body( $response );
$response_body = json_decode( $response_body_content, true );
$error = static::get_wp_error( $response_body, (int) $response_code );
if ( is_wp_error( $error ) ) {
return $error;
}
return $response_body;
}
/**
* Build error object from remote response body and status code.
*
* @param array $response_body Remote response body.
* @param int $response_code Http response code.
* @return WP_Error
*/
protected static function get_wp_error( $response_body, $response_code = 200 ) {
$error_code = null;
foreach ( array( 'code', 'error' ) as $error_code_key ) {
if ( isset( $response_body[ $error_code_key ] ) ) {
$error_code = $response_body[ $error_code_key ];
break;
}
}
// Sometimes the response code could be 200 but the response body still contains an error.
if ( $error_code !== null || $response_code !== 200 ) {
return new WP_Error(
$error_code,
$response_body['message'] ?? 'unknown remote error',
array( 'status' => $response_code )
);
}
// No error.
return null;
}
/**
* Check if the cache should be bypassed.
*
* @return bool
*/
protected static function should_bypass_cache() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return isset( $_GET['force_refresh'] ) || isset( $_GET['statsPurchaseSuccess'] ) ||
// phpcs:ignore WordPress.Arrays.ArrayKeySpacingRestrictions.SpacesAroundArrayKeys, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
( isset( $_SERVER[ 'HTTP_REFERER' ] ) && false !== strpos( $_SERVER[ 'HTTP_REFERER' ], 'force_refresh' ) ) ||
// phpcs:ignore WordPress.Arrays.ArrayKeySpacingRestrictions.SpacesAroundArrayKeys, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
( isset( $_SERVER[ 'HTTP_REFERER' ] ) && false !== strpos( $_SERVER[ 'HTTP_REFERER' ], 'statsPurchaseSuccess' ) );
}
}