Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
62.16% covered (warning)
62.16%
23 / 37
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Dashboard
62.16% covered (warning)
62.16%
23 / 37
71.43% covered (warning)
71.43%
5 / 7
32.66
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 init_hooks
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 add_wp_admin_menu
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 get_capability
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 render
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 get_bootstrap_script
n/a
0 / 0
n/a
0 / 0
1
 admin_init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 load_admin_scripts
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * A class that adds a stats dashboard to wp-admin.
4 *
5 * @package automattic/jetpack-stats-admin
6 */
7
8namespace Automattic\Jetpack\Stats_Admin;
9
10use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
11use Automattic\Jetpack\Stats\Options as Stats_Options;
12
13/**
14 * Responsible for adding a stats dashboard to wp-admin.
15 *
16 * @package jetpack-stats-admin
17 */
18class Dashboard {
19    /**
20     * Whether the class has been initialized
21     *
22     * @var boolean
23     */
24    private static $initialized = false;
25
26    /**
27     * Priority for the dashboard menu
28     * For Jetpack sites: Jetpack uses 998 and 'Admin_Menu' uses 1000, so we need to use 999.
29     * For simple site: the value is overriden in a child class with value 100000 to wait for all menus to be registered.
30     *
31     * @var int
32     */
33    protected $menu_priority = 999;
34
35    /**
36     * Init Stats dashboard.
37     */
38    public static function init() {
39        if ( ! self::$initialized ) {
40            self::$initialized = true;
41            ( new self() )->init_hooks();
42        }
43    }
44
45    /**
46     * Initialize the hooks.
47     */
48    public function init_hooks() {
49        self::$initialized = true;
50        // Jetpack uses 998 and 'Admin_Menu' uses 1000.
51        add_action( 'admin_menu', array( $this, 'add_wp_admin_menu' ), $this->menu_priority );
52    }
53
54    /**
55     * Add a "Stats" top-level admin menu.
56     *
57     * @return void
58     */
59    public function add_wp_admin_menu() {
60        /**
61         * Disable this menu for dashboard.wordpress.com because older versions of Jetpack need to fetch the old Stats UI.
62         *
63         * If this menu is registered, it will conflict with the back-end and break non-odyssey Stats.
64         */
65        if ( defined( 'IS_WPCOM' ) && IS_WPCOM && 120742 === get_current_blog_id() ) {
66            return;
67        }
68
69        $page_suffix = add_menu_page(
70            __( 'Stats', 'jetpack-stats-admin' ),
71            _x( 'Stats', 'product name shown in menu', 'jetpack-stats-admin' ),
72            $this->get_capability(),
73            'stats',
74            array( $this, 'render' ),
75            'dashicons-chart-bar',
76            2
77        );
78
79        if ( $page_suffix ) {
80            add_action( 'load-' . $page_suffix, array( $this, 'admin_init' ) );
81        }
82    }
83
84    /**
85     * Capability a user needs to reach the dashboard.
86     *
87     * Until the site is connected the page exists to pick a plan and connect, which only a user
88     * who can manage the connection can act on. Once connected it is a reporting page, open to
89     * everyone allowed to view stats.
90     *
91     * Pre-connection that is `jetpack_connect`: it maps to `manage_options` on a normal site,
92     * but is `do_not_allow` in offline mode and honours the same multisite/filter rules as the
93     * register endpoint, so the menu is not offered where the connection cannot be completed.
94     *
95     * @return string
96     */
97    protected function get_capability() {
98        return Main::is_site_connected() ? 'view_stats' : 'jetpack_connect';
99    }
100
101    /**
102     * Override render funtion
103     */
104    public function render() {
105        // Record the number of views of the stats dashboard on the initial several loads for the
106        // purpose of showing feedback notice. Views before the site is connected show the plan
107        // choice rather than the dashboard, and there is nothing to give feedback on yet.
108        $views = intval( Stats_Options::get_option( 'views' ) ) + 1;
109        if ( $views <= Notices::VIEWS_TO_SHOW_FEEDBACK && Main::is_site_connected() ) {
110            Stats_Options::set_option( 'views', $views );
111        }
112
113        ?>
114        <div id="wpcom" class="jp-stats-dashboard" style="min-height: calc(100vh - 100px);">
115            <div class="hide-if-js"><?php esc_html_e( 'Your Jetpack Stats dashboard requires JavaScript to function properly.', 'jetpack-stats-admin' ); ?></div>
116            <div class="hide-if-no-js" style="height: 100%">
117                <img
118                    class="jp-stats-dashboard-loading-spinner"
119                    width="32"
120                    height="32"
121                    style="position: absolute; left: 50%; top: 50%;"
122                    alt=<?php echo esc_attr( __( 'Loading', 'jetpack-stats-admin' ) ); ?>
123                    src="//en.wordpress.com/i/loading/loading-64.gif"
124                />
125            </div>
126        </div>
127        <?php
128    }
129
130    /**
131     * The dashboard bootstrap: load the icon sprite, and keep in-app links inside the dashboard.
132     *
133     * @return string
134     */
135    private function get_bootstrap_script() {
136        return <<<'JS'
137jQuery(document).ready(function($) {
138    // Load SVG sprite.
139    $.get("https://widgets.wp.com/odyssey-stats/common/gridicons-506499ddac13811fee8e.svg", function(data) {
140        var div = document.createElement("div");
141        div.innerHTML = new XMLSerializer().serializeToString(data.documentElement);
142        div.style = 'display: none';
143        document.body.insertBefore(div, document.body.childNodes[0]);
144    });
145    // we intercept on all anchor tags and change it to hashbang style.
146    $("#wpcom").on('click', 'a', function (e) {
147        const link = e && e.currentTarget && e.currentTarget.attributes && e.currentTarget.attributes.href && e.currentTarget.attributes.href.value;
148        if( link && link.startsWith( '/stats' ) ) {
149            location.hash = `#!${link}`;
150            return false;
151        }
152    });
153});
154JS;
155    }
156
157    /**
158     * Initialize the admin resources.
159     */
160    public function admin_init() {
161        add_action( 'admin_enqueue_scripts', array( $this, 'load_admin_scripts' ) );
162    }
163
164    /**
165     * Load the admin scripts.
166     */
167    public function load_admin_scripts() {
168        ( new Odyssey_Assets() )->load_admin_scripts( 'jp-stats-dashboard', 'build.min', array( 'config_variable_name' => 'jetpackStatsOdysseyAppConfigData' ) );
169
170        // The bootstrap runs on jQuery, which the Odyssey bundle does not depend on. It gets its own
171        // handle rather than jQuery being added to that bundle, which the dashboard widget shares
172        // and which has no use for it.
173        wp_register_script( 'jp-stats-dashboard-bootstrap', false, array( 'jquery' ), Main::VERSION, true );
174        wp_enqueue_script( 'jp-stats-dashboard-bootstrap' );
175        wp_add_inline_script( 'jp-stats-dashboard-bootstrap', $this->get_bootstrap_script() );
176
177        // The app is served from our CDN and so cannot bundle the connection package. Print the
178        // state Search and Protect print on their own pages, so it can read the connection status
179        // and register the site through the connection REST API itself. Connected sites fetch
180        // `jetpack/v4/connection` over REST instead, and must not receive registrationNonce and
181        // the connected-plugin list on a page that `view_stats` users can open.
182        if ( ! Main::is_site_connected() ) {
183            Connection_Initial_State::render_script( 'jp-stats-dashboard' );
184        }
185    }
186}