Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
7.14% covered (danger)
7.14%
4 / 56
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Analytics
7.14% covered (danger)
7.14%
4 / 56
20.00% covered (danger)
20.00%
1 / 5
195.15
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
42
 is_dashboard_request
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 register_admin_menu
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 register_sidebar_items
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 ensure_script_data
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Analytics package main class.
4 *
5 * @package automattic/jetpack-premium-analytics
6 */
7
8namespace Automattic\Jetpack\PremiumAnalytics;
9
10use Automattic\Jetpack\PremiumAnalytics\REST\Api_Proxy_Controller;
11use Automattic\Jetpack\PremiumAnalytics\REST\Notices_Controller;
12use Automattic\Jetpack\PremiumAnalytics\Sync\Configuration as Sync_Configuration;
13use Automattic\Jetpack\PremiumAnalytics\Sync\Sync_Status_Tracker;
14use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
15
16/**
17 * Main Analytics class.
18 *
19 * Loads the wp-build output and registers an admin page.
20 * The build interceptor handles full-page rendering via admin_init.
21 */
22class Analytics {
23
24    const PACKAGE_VERSION = '0.1.0-alpha';
25
26    /**
27     * Whether the class has been initialized.
28     *
29     * @var bool
30     */
31    private static $initialized = false;
32
33    /**
34     * Menu title for the admin page.
35     *
36     * @var string
37     */
38    private static $menu_title = 'Analytics';
39
40    /**
41     * Initialize the Analytics app.
42     *
43     * @param array $options Optional configuration options.
44     *                       Supported keys:
45     *                       - menu_title (string): Admin menu label.
46     * @return void
47     */
48    public static function init( $options = array() ) {
49        if ( self::$initialized ) {
50            return;
51        }
52
53        self::$initialized = true;
54
55        if ( ! empty( $options['menu_title'] ) ) {
56            self::$menu_title = $options['menu_title'];
57        }
58
59        // Always on: sync runs in cron; REST routes + registry serve REST requests
60        // (is_admin() false). REST_REQUEST isn't defined this early, so they
61        // self-gate on their own rest_api_init / init hooks.
62        Sync_Status_Tracker::configure();
63
64        // TEMPORARY (WOOA7S-1550): register the interim woocommerce_analytics sync module so
65        // Sync_Status_Tracker has a full sync to observe. Remove when the shared sync-modules package lands.
66        Sync_Configuration::register();
67        Api_Proxy_Controller::register();
68        Notices_Controller::register();
69
70        // Load the widget type registry: hydration routine, registry-time and
71        // runtime filters, and the registry accessors.
72        require_once __DIR__ . '/widget-types.php';
73
74        // Apply Premium Analytics' availability policy: hooks the registry-time
75        // filter to keep developer-only types out of production.
76        require_once __DIR__ . '/widget-availability.php';
77
78        // Hydrate the registry with the availability filter in place.
79        bootstrap_widget_types();
80
81        // Expose dashboard widget modules over REST and wire them into the
82        // page import map for dynamic import() on the client.
83        require_once __DIR__ . '/widget-modules.php';
84
85        // Register the dashboard's default layout: the first-load preference
86        // injection and the REST route the "reset to default" action reads.
87        require_once __DIR__ . '/dashboard-layout.php';
88
89        // Load wp-build output (interceptor, modules, routes, page render).
90        // Must stay above the is_admin() gate: build/widgets.php defines the
91        // manifest the widget registry reads, and the registry serves REST
92        // requests (e.g. /jetpack/v4/widget-modules) where is_admin() is false. The render
93        // pieces here self-gate on admin_init, so loading them globally is inert
94        // off the dashboard. Only the polyfill registration below is admin-scoped.
95        $build_entry = __DIR__ . '/../build/build.php';
96        if ( file_exists( $build_entry ) ) {
97            require_once $build_entry;
98        }
99
100        // Below: admin-only render path (assets, menu).
101        if ( ! is_admin() ) {
102            return;
103        }
104
105        // Polyfills force-replace core handles (wp-private-apis) on wp_default_scripts;
106        // scope to the dashboard page so no other admin page (e.g. block editor) is hit.
107        if ( self::is_dashboard_request() ) {
108            WP_Build_Polyfills::register(
109                'jetpack-premium-analytics',
110                array_merge(
111                    WP_Build_Polyfills::SCRIPT_HANDLES,
112                    WP_Build_Polyfills::MODULE_IDS
113                )
114            );
115        }
116
117        add_action( 'admin_menu', array( static::class, 'register_admin_menu' ) );
118        add_action( 'jetpack-premium-analytics_init', array( static::class, 'register_sidebar_items' ) );
119        add_action( 'jetpack-premium-analytics_init', array( static::class, 'ensure_script_data' ) );
120    }
121
122    /**
123     * Admin page slugs that render the Premium Analytics dashboard.
124     *
125     * Mirrors the slugs the wp-build interceptor renders (full-page and the
126     * wp-admin integrated variant).
127     */
128    const DASHBOARD_PAGE_SLUGS = array( 'jetpack-premium-analytics', 'jetpack-premium-analytics-wp-admin' );
129
130    /**
131     * Whether the current request is rendering a Premium Analytics dashboard page.
132     *
133     * Used to scope the wp-build polyfill registration (which force-replaces core
134     * script handles) to this dashboard, so it never affects other admin pages.
135     * Must be cheap and safe to call at plugin-load time, before current_screen
136     * exists, so it reads the menu page slug directly like the build interceptor does.
137     *
138     * @return bool True when serving a dashboard page in wp-admin.
139     */
140    public static function is_dashboard_request() {
141        if ( ! is_admin() ) {
142            return false;
143        }
144
145        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the menu page slug to scope asset loading; no state is changed.
146        $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
147
148        return in_array( $page, self::DASHBOARD_PAGE_SLUGS, true );
149    }
150
151    /**
152     * Register the admin menu page.
153     *
154     * Uses the wp-build "wp-admin integrated" variant (`-wp-admin` slug) so the
155     * dashboard renders inside the native wp-admin shell, not the full-page
156     * variant that takes over the screen via admin_init. The render callback
157     * comes from the generated build, with a no-op fallback when it is absent.
158     *
159     * @return void
160     */
161    public static function register_admin_menu() {
162        $render_callback = function_exists( 'jpa_jetpack_premium_analytics_wp_admin_render_page' )
163            ? 'jpa_jetpack_premium_analytics_wp_admin_render_page'
164            : '__return_null';
165
166        add_menu_page(
167            esc_html( self::$menu_title ),
168            esc_html( self::$menu_title ),
169            'manage_options',
170            'jetpack-premium-analytics-wp-admin',
171            $render_callback,
172            'dashicons-chart-bar',
173            30
174        );
175    }
176
177    /**
178     * Register sidebar menu items for the full-page app.
179     *
180     * @return void
181     */
182    public static function register_sidebar_items() {
183        if ( ! function_exists( 'jpa_register_jetpack_premium_analytics_menu_item' ) ) {
184            return;
185        }
186
187        // @phan-suppress-next-line PhanUndeclaredFunction -- Guarded by function_exists() above.
188        jpa_register_jetpack_premium_analytics_menu_item(
189            'dashboard',
190            __( 'Dashboard', 'jetpack-premium-analytics' ),
191            '/'
192        );
193    }
194
195    /**
196     * Emit window.JetpackScriptData on the boot-rendered admin page.
197     *
198     * The wp-build interceptor that renders this page (its page.php template)
199     * reproduces wp-admin/admin-header.php but does not fire the
200     * `admin_print_scripts` action. The jetpack-assets Script_Data class hooks
201     * that action to print `window.JetpackScriptData` — which carries the
202     * connection data the route guards read — so without help the global is
203     * never emitted and the guards cannot tell whether the site is connected.
204     *
205     * Hooked on the page's own init action, this runs only for this page, in
206     * time for the footer scripts to print. Script_Data guards against rendering
207     * twice, so it is a no-op wherever `admin_print_scripts` fires normally.
208     *
209     * @return void
210     */
211    public static function ensure_script_data() {
212        $script_data = 'Automattic\Jetpack\Assets\Script_Data';
213        if ( is_callable( array( $script_data, 'render_script_data' ) ) ) {
214            $script_data::render_script_data();
215        }
216    }
217}