PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
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-dashboard.php +124 -230 12.0.3 → 16.3-beta View file →
@@ -7,12 +7,11 @@
7 7
8 8 namespace Automattic\Jetpack\Stats_Admin;
9 9
10 10 use Automattic\Jetpack\Admin_UI\Admin_Menu;
11 -use Automattic\Jetpack\Assets;
12 -use Automattic\Jetpack\Modules;
11 +use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
12 +use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
13 13 use Automattic\Jetpack\Stats\Options as Stats_Options;
14 -use Jetpack_Options;
15 14
16 15 /**
17 16 * Responsible for adding a stats dashboard to wp-admin.
18 17 *
@@ -18,30 +17,28 @@
18 17 *
19 18 * @package jetpack-stats-admin
20 19 */
21 20 class Dashboard {
22 - // This is a fixed list @see https://github.com/Automattic/wp-calypso/pull/71442/
23 - 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' );
24 - const ODYSSEY_CDN_URL = 'https://widgets.wp.com/odyssey-stats/%s/%s';
25 - const OPT_OUT_NEW_STATS_FEATURE_CLASS = 'opt-out-new-stats';
26 21 /**
27 - * We bump the asset version when the Jetpack back end is not compatible anymore.
22 + * Whether the class has been initialized
23 + *
24 + * @var boolean
28 25 */
29 - const ODYSSEY_STATS_VERSION = 'v1';
30 - const ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY = 'odyssey_stats_admin_asset_cache_buster';
26 + private static $initialized = false;
31 27
32 28 /**
33 - * Whether the class has been initialized
29 + * Transient that throttles the plan refresh below.
34 30 *
35 - * @var boolean
31 + * @var string
36 32 */
37 - private static $initialized = false;
33 + private const PLAN_REFRESH_TRANSIENT = 'jetpack_stats_admin_plan_refresh';
38 34
39 35 /**
40 36 * Priority for the dashboard menu
41 37 * For Jetpack sites: Jetpack uses 998 and 'Admin_Menu' uses 1000, so we need to use 999.
42 - * For simple site: the value is overriden in a child class with value 100000 to wait for all menus to be registered.
43 38 *
39 + * Admin_Menu registers what it has queued at priority 1000, so this has to stay below it.
40 + *
44 41 * @var int
45 42 */
46 43 protected $menu_priority = 999;
47 44
@@ -60,36 +57,77 @@
60 57 */
61 58 public function init_hooks() {
62 59 self::$initialized = true;
63 60 // Jetpack uses 998 and 'Admin_Menu' uses 1000.
64 - add_action( 'admin_menu', array( $this, 'add_wp_admin_submenu' ), $this->menu_priority );
61 + add_action( 'admin_menu', array( $this, 'add_wp_admin_menu' ), $this->menu_priority );
65 62 }
66 63
67 64 /**
68 - * The page to be added to submenu
65 + * Add a "Stats" top-level admin menu.
66 + *
67 + * Declares no `product` gate: that resolves false without the Jetpack plugin, which is
68 + * exactly when the standalone Stats plugin registers this page.
69 + *
70 + * @return void
69 71 */
70 - public function add_wp_admin_submenu() {
71 - $page_suffix = Admin_Menu::add_menu(
72 - __( 'Stats App', 'jetpack-stats-admin' ),
73 - _x( 'Stats App', 'product name shown in menu', 'jetpack-stats-admin' ),
74 - 'manage_options',
75 - 'jetpack-stats-app',
76 - array( $this, 'render' ),
77 - 100
78 - );
72 + public function add_wp_admin_menu() {
73 + /**
74 + * Disable this menu for dashboard.wordpress.com because older versions of Jetpack need to fetch the old Stats UI.
75 + *
76 + * If this menu is registered, it will conflict with the back-end and break non-odyssey Stats.
77 + */
78 + if ( defined( 'IS_WPCOM' ) && IS_WPCOM && 120742 === get_current_blog_id() ) {
79 + return;
80 + }
79 81
82 + $page_title = __( 'Stats', 'jetpack-stats-admin' );
83 + $menu_title = _x( 'Stats', 'product name shown in menu', 'jetpack-stats-admin' );
84 + $capability = $this->get_capability();
85 + $callback = array( $this, 'render' );
86 +
87 + // An older admin-ui, loaded first by another plugin, may predate add_top_level_menu().
88 + if ( method_exists( Admin_Menu::class, 'add_top_level_menu' ) ) {
89 + // The key the legacy Stats screen in the Jetpack plugin also declares, so hosts name Stats once.
90 + $page_suffix = Admin_Menu::add_top_level_menu( $page_title, $menu_title, $capability, 'stats', $callback, 'dashicons-chart-bar', 2, array( 'key' => 'jetpack-stats' ) );
91 + } else {
92 + $page_suffix = add_menu_page( $page_title, $menu_title, $capability, 'stats', $callback, 'dashicons-chart-bar', 2 );
93 + }
94 +
80 95 if ( $page_suffix ) {
81 96 add_action( 'load-' . $page_suffix, array( $this, 'admin_init' ) );
97 + // The dashboard renders full bleed, so core notices stacked above it look broken.
98 + if ( method_exists( Admin_Menu::class, 'hide_core_admin_notices' ) ) {
99 + add_action( 'load-' . $page_suffix, array( Admin_Menu::class, 'hide_core_admin_notices' ) );
100 + }
82 101 }
83 102 }
84 103
85 104 /**
105 + * Capability a user needs to reach the dashboard.
106 + *
107 + * Until the site is connected the page exists to pick a plan and connect, which only a user
108 + * who can manage the connection can act on. Once connected it is a reporting page, open to
109 + * everyone allowed to view stats.
110 + *
111 + * Pre-connection that is `jetpack_connect`: it maps to `manage_options` on a normal site,
112 + * but is `do_not_allow` in offline mode and honours the same multisite/filter rules as the
113 + * register endpoint, so the menu is not offered where the connection cannot be completed.
114 + *
115 + * @return string
116 + */
117 + protected function get_capability() {
118 + return Main::is_site_connected() ? 'view_stats' : 'jetpack_connect';
119 + }
120 +
121 + /**
86 122 * Override render funtion
87 123 */
88 124 public function render() {
89 - // Record the number of views of the stats dashboard on the initial several loads for the purpose of showing feedback notice.
125 + // Record the number of views of the stats dashboard on the initial several loads for the
126 + // purpose of showing feedback notice. Views before the site is connected show the plan
127 + // choice rather than the dashboard, and there is nothing to give feedback on yet.
90 128 $views = intval( Stats_Options::get_option( 'views' ) ) + 1;
91 - if ( $views <= Notices::VIEWS_TO_SHOW_FEEDBACK ) {
129 + if ( $views <= Notices::VIEWS_TO_SHOW_FEEDBACK && Main::is_site_connected() ) {
92 130 Stats_Options::set_option( 'views', $views );
93 131 }
94 132
95 133 ?>
@@ -105,236 +143,92 @@
105 143 src="//en.wordpress.com/i/loading/loading-64.gif"
106 144 />
107 145 </div>
108 146 </div>
109 - <script>
110 - jQuery(document).ready(function($) {
111 - // Load SVG sprite.
112 - $.get("https://widgets.wp.com/odyssey-stats/common/gridicons-506499ddac13811fee8e.svg", function(data) {
113 - var div = document.createElement("div");
114 - div.innerHTML = new XMLSerializer().serializeToString(data.documentElement);
115 - div.style = 'display: none';
116 - document.body.insertBefore(div, document.body.childNodes[0]);
117 - });
118 - // we intercept on all anchor tags and change it to hashbang style.
119 - $("#wpcom").on('click', 'a', function (e) {
120 - const link = e && e.currentTarget && e.currentTarget.attributes && e.currentTarget.attributes.href && e.currentTarget.attributes.href.value;
121 - if( link && link.startsWith( '/stats' ) ) {
122 - location.hash = `#!${link}`;
123 - return false;
124 - }
125 - });
126 - });
127 - </script>
128 147 <?php
129 148 }
130 149
131 150 /**
132 - * Initialize the admin resources.
151 + * The dashboard bootstrap: load the icon sprite, and keep in-app links inside the dashboard.
152 + *
153 + * @return string
133 154 */
134 - public function admin_init() {
135 - add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_scripts' ) );
155 + private function get_bootstrap_script() {
156 + return <<<'JS'
157 +jQuery(document).ready(function($) {
158 + // Load SVG sprite.
159 + $.get("https://widgets.wp.com/odyssey-stats/common/gridicons-506499ddac13811fee8e.svg", function(data) {
160 + var div = document.createElement("div");
161 + div.innerHTML = new XMLSerializer().serializeToString(data.documentElement);
162 + div.style = 'display: none';
163 + document.body.insertBefore(div, document.body.childNodes[0]);
164 + });
165 + // we intercept on all anchor tags and change it to hashbang style.
166 + $("#wpcom").on('click', 'a', function (e) {
167 + const link = e && e.currentTarget && e.currentTarget.attributes && e.currentTarget.attributes.href && e.currentTarget.attributes.href.value;
168 + if( link && link.startsWith( '/stats' ) ) {
169 + location.hash = `#!${link}`;
170 + return false;
171 + }
172 + });
173 +});
174 +JS;
136 175 }
137 176
138 177 /**
139 - * Enqueue admin scripts.
178 + * Initialize the admin resources.
140 179 */
141 - public function load_admin_scripts() {
142 - if ( file_exists( __DIR__ . '/../dist/build.min.js' ) ) {
143 - // Load local assets for the convinience of development.
144 - Assets::register_script(
145 - 'jp-stats-dashboard',
146 - '../dist/build.min.js',
147 - __FILE__,
148 - array(
149 - 'in_footer' => true,
150 - 'textdomain' => 'jetpack-stats-admin',
151 - )
152 - );
153 - Assets::enqueue_script( 'jp-stats-dashboard' );
154 - } else {
155 - // In production, we load the assets from our CDN.
156 - $css_url = 'build.min' . ( is_rtl() ? '.rtl' : '' ) . '.css';
157 - wp_register_script( 'jp-stats-dashboard', sprintf( self::ODYSSEY_CDN_URL, self::ODYSSEY_STATS_VERSION, 'build.min.js' ), self::JS_DEPENDENCIES, $this->get_cdn_asset_cache_buster(), true );
158 - wp_register_style( 'jp-stats-dashboard-style', sprintf( self::ODYSSEY_CDN_URL, self::ODYSSEY_STATS_VERSION, $css_url ), array(), $this->get_cdn_asset_cache_buster() );
159 - wp_enqueue_script( 'jp-stats-dashboard' );
160 - wp_enqueue_style( 'jp-stats-dashboard-style' );
161 - }
162 -
163 - wp_add_inline_script(
164 - 'jp-stats-dashboard',
165 - $this->get_config_data_js(),
166 - 'before'
167 - );
180 + public function admin_init() {
181 + $this->maybe_refresh_plan();
182 + add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_scripts' ) );
168 183 }
169 184
170 185 /**
171 - * Returns cache buster string for assets.
172 - * Development mode doesn't need this, as it's handled by `Assets` class.
186 + * Fill an empty plan cache before the config data that reads it is printed.
187 + *
188 + * The app cannot refresh the plan it paywalls on, so a site that never stored one renders as
189 + * free. Throttled and time-boxed, because WordPress.com can keep answering without a plan.
173 190 */
174 - protected function get_cdn_asset_cache_buster() {
175 - // Use cached cache buster in production.
176 - $remote_asset_version = get_transient( self::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY );
177 - if ( ! empty( $remote_asset_version ) ) {
178 - return $remote_asset_version;
191 + private function maybe_refresh_plan() {
192 + if ( ! Main::is_site_connected() ) {
193 + return;
179 194 }
180 195
181 - // If no cached cache buster, we fetch it from CDN and set to transient.
182 - $response = wp_remote_get( sprintf( self::ODYSSEY_CDN_URL, self::ODYSSEY_STATS_VERSION, 'build_meta.json' ), array( 'timeout' => 5 ) );
183 -
184 - if ( is_wp_error( $response ) ) {
185 - // fallback to the package version.
186 - return Main::VERSION;
196 + // method_exists guard: an older plans package may win the autoloader on another plugin.
197 + if ( method_exists( Jetpack_Plan::class, 'get_wpcom_site_specific_features' )
198 + && null !== Jetpack_Plan::get_wpcom_site_specific_features() ) {
199 + return;
187 200 }
188 201
189 - $build_meta = json_decode( wp_remote_retrieve_body( $response ), true );
190 - if ( ! empty( $build_meta['cache_buster'] ) ) {
191 - // Cache the cache buster for a day.
192 - set_transient( self::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY, $build_meta['cache_buster'], 15 * MINUTE_IN_SECONDS );
193 - return $build_meta['cache_buster'];
202 + $plan = Jetpack_Plan::get();
203 + if ( ! empty( $plan['features']['active'] ) || get_transient( self::PLAN_REFRESH_TRANSIENT ) ) {
204 + return;
194 205 }
195 206
196 - // fallback to the package version.
197 - return Main::VERSION;
198 - }
207 + set_transient( self::PLAN_REFRESH_TRANSIENT, 1, 15 * MINUTE_IN_SECONDS );
199 208
200 - /**
201 - * Set configData to window.configData.
202 - */
203 - protected function get_config_data_js() {
204 - return 'window.configData = ' . wp_json_encode(
205 - $this->config_data()
206 - ) . ';';
209 + Jetpack_Plan::refresh_from_wpcom( array( 'timeout' => 5 ) );
207 210 }
208 211
209 212 /**
210 - * Return the config for the app.
213 + * Load the admin scripts.
211 214 */
212 - protected function config_data() {
213 - $blog_id = Jetpack_Options::get_option( 'id' );
214 - $empty_object = json_decode( '{}' );
215 - return array(
216 - 'admin_page_base' => static::get_admin_path(),
217 - 'api_root' => esc_url_raw( rest_url() ),
218 - 'blog_id' => Jetpack_Options::get_option( 'id' ),
219 - 'enable_all_sections' => false,
220 - 'env_id' => 'production',
221 - 'google_analytics_key' => 'UA-10673494-15',
222 - 'google_maps_and_places_api_key' => '',
223 - 'hostname' => wp_parse_url( get_site_url(), PHP_URL_HOST ),
224 - 'i18n_default_locale_slug' => 'en',
225 - 'i18n_locale_slug' => static::get_site_locale(),
226 - 'mc_analytics_enabled' => false,
227 - 'meta' => array(),
228 - 'nonce' => wp_create_nonce( 'wp_rest' ),
229 - 'site_name' => \get_bloginfo( 'name' ),
230 - 'sections' => array(),
231 - // Features are inlined @see https://github.com/Automattic/wp-calypso/pull/70122
232 - 'features' => array(),
233 - 'intial_state' => array(
234 - 'currentUser' => array(
235 - 'id' => 1000,
236 - 'user' => array(
237 - 'ID' => 1000,
238 - 'username' => 'no-user',
239 - ),
240 - 'capabilities' => array(
241 - "$blog_id" => self::get_current_user_capabilities(),
242 - ),
243 - ),
244 - 'sites' => array(
245 - 'items' => array(
246 - "$blog_id" => array(
247 - 'ID' => $blog_id,
248 - 'URL' => site_url(),
249 - 'jetpack' => true,
250 - 'visible' => true,
251 - 'capabilities' => $empty_object,
252 - 'products' => array(),
253 - 'plan' => $empty_object, // we need this empty object, otherwise the front end would crash on insight page.
254 - 'options' => array(
255 - 'wordads' => ( new Modules() )->is_active( 'wordads' ),
256 - 'admin_url' => admin_url(),
257 - ),
258 - 'stats_notices' => ( new Notices() )->get_notices_to_show(),
259 - ),
260 - ),
261 - 'features' => array( "$blog_id" => array( 'data' => self::get_plan_features() ) ),
262 - ),
263 - ),
264 - );
265 - }
215 + public function load_admin_scripts() {
216 + ( new Odyssey_Assets() )->load_admin_scripts( 'jp-stats-dashboard', 'build.min', array( 'config_variable_name' => 'jetpackStatsOdysseyAppConfigData' ) );
266 217
267 - /**
268 - * Page base for the Calypso admin page.
269 - */
270 - protected static function get_admin_path() {
271 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
272 - if ( ! isset( $_SERVER['PHP_SELF'] ) || ! isset( $_SERVER['QUERY_STRING'] ) ) {
273 - $parsed = wp_parse_url( admin_url( 'admin.php?page=stats' ) );
274 - return $parsed['path'] . '?' . $parsed['query'];
275 - }
276 - // We do this because page.js requires the exactly page base to be set otherwise it will not work properly.
277 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
278 - return wp_unslash( $_SERVER['PHP_SELF'] ) . '?' . wp_unslash( $_SERVER['QUERY_STRING'] );
279 - }
218 + // The bootstrap runs on jQuery, which the Odyssey bundle does not depend on. It gets its own
219 + // handle rather than jQuery being added to that bundle, which the dashboard widget shares
220 + // and which has no use for it.
221 + wp_register_script( 'jp-stats-dashboard-bootstrap', false, array( 'jquery' ), Main::VERSION, true );
222 + wp_enqueue_script( 'jp-stats-dashboard-bootstrap' );
223 + wp_add_inline_script( 'jp-stats-dashboard-bootstrap', $this->get_bootstrap_script() );
280 224
281 - /**
282 - * Get locale acceptable by Calypso.
283 - */
284 - protected static function get_site_locale() {
285 - // Stolen from `projects/plugins/jetpack/modules/sitemaps/sitemap-builder.php`
286 -
287 - /*
288 - * Trim the locale to an ISO 639 language code as required by Google.
289 - * Special cases are zh-cn (Simplified Chinese) and zh-tw (Traditional Chinese).
290 - * @link https://www.loc.gov/standards/iso639-2/php/code_list.php
291 - */
292 - $locale = strtolower( get_locale() );
293 -
294 - if ( in_array( $locale, array( 'zh_tw', 'zh_cn' ), true ) ) {
295 - $locale = str_replace( '_', '-', $locale );
296 - } else {
297 - $locale = preg_replace( '/(_.*)$/i', '', $locale );
225 + // The app is served from our CDN and so cannot bundle the connection package. Print the
226 + // state Search and Protect print on their own pages, so it can read the connection status
227 + // and register the site through the connection REST API itself. Connected sites fetch
228 + // `jetpack/v4/connection` over REST instead, and must not receive registrationNonce and
229 + // the connected-plugin list on a page that `view_stats` users can open.
230 + if ( ! Main::is_site_connected() ) {
231 + Connection_Initial_State::render_script( 'jp-stats-dashboard' );
298 232 }
299 - return $locale;
300 233 }
301 -
302 - /**
303 - * Get the features of the current plan.
304 - */
305 - protected static function get_plan_features() {
306 - if ( ! class_exists( 'Jetpack_Plan' ) ) {
307 - return array();
308 - }
309 - $plan = \Jetpack_Plan::get();
310 - if ( empty( $plan['features'] ) ) {
311 - return array();
312 - }
313 - return $plan['features'];
314 - }
315 -
316 - /**
317 - * Get the capabilities of the current user.
318 - *
319 - * @return array An array of capabilities.
320 - */
321 - protected static function get_current_user_capabilities() {
322 - $user = wp_get_current_user();
323 - if ( ! $user || is_wp_error( $user ) ) {
324 - return array();
325 - }
326 - return $user->allcaps;
327 - }
328 -
329 - /**
330 - * Return ture if the opt-out notice should be shown.
331 - *
332 - * @return bool
333 - */
334 - protected static function has_opt_out_new_stats_notice() {
335 - $new_stats_enabled = Stats_Options::get_option( 'enable_odyssey_stats' );
336 - $hidden_jitms = \Jetpack_Options::get_option( 'hide_jitm' );
337 - return $new_stats_enabled && ! isset( $hidden_jitms[ self::OPT_OUT_NEW_STATS_FEATURE_CLASS ] );
338 - }
339 -
340 234 }