Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.86% covered (warning)
83.86%
613 / 731
73.13% covered (warning)
73.13%
49 / 67
CRAP
0.00% covered (danger)
0.00%
0 / 1
Initializer
83.86% covered (warning)
83.86%
613 / 731
73.13% covered (warning)
73.13%
49 / 67
547.53
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
5
 use_local_module_options
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 restore_module_options
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 is_licensing_ui_enabled
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 add_my_jetpack_menu_item
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 current_user_can_access_page
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 admin_init
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
5.01
 is_onboarding_settled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 maybe_redirect_to_onboarding
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
7.01
 is_onboarding_available
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 get_partner_coupon_screen
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
8
 get_onboarding_redirect_args
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
12
 silence_onboarding_notices
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 hide_onboarding_jitms
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 add_onboarding_admin_body_class
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 can_use_analytics
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 is_onboarding_wizard_enabled
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 has_never_registered_jetpack
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_onboarding_wizard_state
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 is_my_jetpack_admin_request
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 is_onboarding_request
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 is_onboarding_takeover
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 alias_screen_id_for_wp_build
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 restore_screen_id_after_wp_build
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 should_load_wp_build
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 should_render_wp_build
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 maybe_load_wp_build
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 load_wp_build
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 require_wp_build_with_screen_alias
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
2.06
 enqueue_scripts
96.90% covered (success)
96.90%
125 / 129
0.00% covered (danger)
0.00%
0 / 1
12
 get_header_state
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 add_script_data
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 is_modules_rest_api_enabled
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 get_modules_management_url
27.14% covered (danger)
27.14%
19 / 70
0.00% covered (danger)
0.00%
0 / 1
691.11
 print_modules_fallback
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
9
 get_products_section
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 add_admin_script_data
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
 get_registered_admin_page_slugs
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 is_admin_page_available
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 get_assets_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_installed_jetpack_plugins
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 get_active_modules
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 is_jetpack_user_new
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
56
 register_feature_flags
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
 is_header_controls_enabled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 maybe_enqueue_header_controls_script
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 get_my_jetpack_flags
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 is_overview_v1_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_seo_opt_in_state
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 admin_page
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 register_rest_endpoints
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
2
 permissions_callback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 should_initialize
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
7
 is_offline_features_enabled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setup_historically_active_jetpack_modules_sync
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 get_site
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 get_site_info
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
5.16
 is_commercial_site
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 is_registered
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dismiss_welcome_banner
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 has_file_system_write_access
61.11% covered (warning)
61.11%
11 / 18
0.00% covered (danger)
0.00%
0 / 1
8.12
 get_idc_container_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 maybe_show_red_bubble
96.23% covered (success)
96.23%
51 / 53
0.00% covered (danger)
0.00%
0 / 1
12
 has_connection_error
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 get_alerts_counted_elsewhere
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 enqueue_red_bubble_script
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 get_recommended_modules
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * WP Admin page with information and configuration shared among all Jetpack stand-alone plugins
4 *
5 * @package automattic/my-jetpack
6 */
7
8namespace Automattic\Jetpack\My_Jetpack;
9
10use Automattic\Jetpack\Admin_UI\Admin_Menu;
11use Automattic\Jetpack\Assets;
12use Automattic\Jetpack\Boost_Speed_Score\Speed_Score;
13use Automattic\Jetpack\Boost_Speed_Score\Speed_Score_History;
14use Automattic\Jetpack\Connection\Client;
15use Automattic\Jetpack\Connection\Error_Handler as Connection_Error_Handler;
16use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
17use Automattic\Jetpack\Connection\Manager as Connection_Manager;
18use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication;
19use Automattic\Jetpack\Connection\REST_Jetpack_AI_JWT;
20use Automattic\Jetpack\Constants as Jetpack_Constants;
21use Automattic\Jetpack\ExPlat;
22use Automattic\Jetpack\Feature_Flags\Feature_Flags;
23use Automattic\Jetpack\JITMS\JITM;
24use Automattic\Jetpack\Licensing;
25use Automattic\Jetpack\Menu_Badges\Menu_Badges;
26use Automattic\Jetpack\Menu_Badges\Notification_Counts;
27use Automattic\Jetpack\Modules;
28use Automattic\Jetpack\Partner_Coupon;
29use Automattic\Jetpack\Plugins_Installer;
30use Automattic\Jetpack\Status;
31use Automattic\Jetpack\Status\Host as Status_Host;
32use Automattic\Jetpack\Sync\Functions as Sync_Functions;
33use Automattic\Jetpack\Terms_Of_Service;
34use Automattic\Jetpack\Tracking;
35use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
36use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id;
37use Jetpack;
38use WP_Error;
39
40/**
41 * The main Initializer class that registers the admin menu and eneuque the assets.
42 */
43class Initializer {
44
45    /**
46     * My Jetpack package version
47     *
48     * @var string
49     */
50    const PACKAGE_VERSION = '6.11.0';
51
52    /**
53     * Feature flag that swaps the single-screen onboarding takeover for the setup wizard.
54     */
55    const ONBOARDING_WIZARD_FEATURE_FLAG = 'my-jetpack-onboarding-wizard';
56
57    /**
58     * User option recording that this user chose to skip setup.
59     *
60     * Per user rather than per site, because skipping is one person saying "not
61     * now": a second admin arriving at a site nobody has set up should still be
62     * offered the flow. Finishing it is the site-wide fact, and that is
63     * ONBOARDING_COMPLETED_OPTION.
64     *
65     * A user option rather than plain user meta: `wp_usermeta` is network-wide, so
66     * without the blog prefix a skip on one site would silence setup on all of them.
67     */
68    const ONBOARDING_DISMISSED_USER_OPTION = 'jetpack_my_jetpack_onboarding_dismissed';
69
70    /**
71     * Option recording that someone has finished setup on this site.
72     *
73     * Site-wide, because the flow connects the site and switches modules on, so a
74     * second admin does not need it again. Deliberately not autoloaded.
75     */
76    const ONBOARDING_COMPLETED_OPTION = 'jetpack_my_jetpack_onboarding_completed';
77
78    /**
79     * Handle for the classic script that carries the React initial state.
80     *
81     * Plugins that render My Jetpack components on their own pages (Boost) depend on this name.
82     */
83    const DATA_SCRIPT_HANDLE = 'my_jetpack_main_app';
84
85    /**
86     * Handle for the webpack bundle that renders the onboarding takeover.
87     */
88    const ONBOARDING_SCRIPT_HANDLE = 'my_jetpack_onboarding';
89
90    /**
91     * Handle for the webpack bundle that gives dashboards outside My Jetpack the shared header's controls.
92     */
93    const HEADER_CONTROLS_SCRIPT_HANDLE = 'my-jetpack-header-controls';
94
95    /**
96     * The wp-build page ID, which the generated enqueue check expects as the screen ID.
97     */
98    const WP_BUILD_PAGE_ID = 'my-jetpack-dashboard';
99
100    /**
101     * HTML container ID for the IDC screen on My Jetpack page.
102     */
103    private const IDC_CONTAINER_ID = 'my-jetpack-identity-crisis-container';
104
105    public const JETPACK_PLUGIN_SLUGS = array(
106        'jetpack-backup',
107        'jetpack-boost',
108        'zerobscrm',
109        'jetpack',
110        'jetpack-protect',
111        'jetpack-social',
112        'jetpack-videopress',
113        'jetpack-search',
114        'jetpack-stats',
115    );
116
117    private const MY_JETPACK_SITE_INFO_TRANSIENT_KEY = 'my-jetpack-site-info';
118
119    /**
120     * Feature flag for the shared header's global controls.
121     *
122     * @var string
123     */
124    public const HEADER_CONTROLS_FLAG = 'my-jetpack-header-controls';
125
126    /**
127     * Feature flag for Overview v1.
128     *
129     * @var string
130     */
131    public const OVERVIEW_V1_FLAG = 'my-jetpack-overview-v1';
132
133    /**
134     * Feature flag for sending classic Modules links to Features.
135     *
136     * @var string
137     */
138    public const MODULES_TO_FEATURES_FLAG = 'my-jetpack-modules-to-features';
139
140    /**
141     * Holds info/data about the site (from the /sites/%d endpoint)
142     *
143     * @var object
144     */
145    public static $site_info;
146
147    /**
148     * The screen ID alias_screen_id_for_wp_build() replaced, until it is restored.
149     *
150     * @var string|null
151     */
152    private static $wp_build_original_screen_id = null;
153
154    /**
155     * Initialize My Jetpack
156     *
157     * @return void
158     */
159    public static function init() {
160        self::register_feature_flags();
161
162        // Before the gate: the Jetpack plugin renders this package's connection screen and footer
163        // links even where My Jetpack is off, and `myJetpackInitialState` only exists on its own page.
164        add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'add_admin_script_data' ) );
165        add_action( 'admin_head', array( __CLASS__, 'print_modules_fallback' ) );
166
167        if ( ! self::should_initialize() || did_action( 'my_jetpack_init' ) ) {
168            return;
169        }
170
171        if ( self::is_offline_features_enabled() ) {
172            add_filter( 'jetpack_my_jetpack_should_enable_add_license_screen', '__return_false' );
173            Connection_Rest_Authentication::init();
174            add_filter( 'rest_request_before_callbacks', array( __CLASS__, 'use_local_module_options' ), 10, 3 );
175            add_filter( 'rest_request_after_callbacks', array( __CLASS__, 'restore_module_options' ), 10, 3 );
176            add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) );
177            add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 );
178            add_action( 'admin_menu', array( __CLASS__, 'add_my_jetpack_menu_item' ) );
179            do_action( 'my_jetpack_init' );
180            return;
181        }
182
183        // Answer "is this product on?" for admin menu registration.
184        Menu_Visibility::init();
185
186        // Extend jetpack plugins action links.
187        Products::extend_plugins_action_links();
188
189        // Set up the REST authentication hooks.
190        Connection_Rest_Authentication::init();
191
192        if ( self::is_licensing_ui_enabled() ) {
193            Licensing::instance()->initialize();
194        }
195
196        // Initialize Boost Speed Score
197        new Speed_Score( array(), 'jetpack-my-jetpack' );
198
199        // Add custom WP REST API endoints.
200        add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) );
201
202        // Both of wp-build's deadlines fall later in this request: its enqueue check must be
203        // hooked before `admin_enqueue_scripts`, and its render function defined before
204        // the page callback.
205        add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 );
206
207        add_action( 'admin_menu', array( __CLASS__, 'add_my_jetpack_menu_item' ) );
208
209        add_action( 'admin_init', array( __CLASS__, 'setup_historically_active_jetpack_modules_sync' ) );
210
211        // Nothing else gets to talk over the takeover. See both methods for why it
212        // takes two hooks rather than one.
213        add_action( 'in_admin_header', array( __CLASS__, 'silence_onboarding_notices' ), PHP_INT_MAX );
214        add_filter( 'jetpack_display_jitms_on_screen', array( __CLASS__, 'hide_onboarding_jitms' ), 10, 2 );
215        // Registered on admin_menu (not admin_init) and well before priority 100000, so the
216        // counts it registers exist before the menu-badges renderer runs on admin_menu 100000.
217        add_action( 'admin_menu', array( __CLASS__, 'maybe_show_red_bubble' ), 30 );
218
219        // Set up the ExPlat package endpoints
220        ExPlat::init();
221
222        // Sets up JITMS.
223        JITM::configure();
224
225        // Add "Jetpack Manage" menu item.
226        Jetpack_Manage::init();
227
228        /**
229         * Fires after the My Jetpack package is initialized
230         *
231         * @since 0.1.0
232         */
233        do_action( 'my_jetpack_init' );
234    }
235
236    /**
237     * Keep uncached module-list options local on the offline route.
238     *
239     * @since 6.10.0
240     * @param mixed            $response Response before the callback.
241     * @param array            $handler  Route handler.
242     * @param \WP_REST_Request $request  REST request.
243     * @return mixed
244     */
245    public static function use_local_module_options( $response, $handler, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- WordPress REST filter signature.
246        if ( '/jetpack/v4/module/all' === $request->get_route() ) {
247            add_filter( 'default_option_monitor_receive_notifications', '__return_false' );
248            add_filter( 'default_option_post_by_email_address' . get_current_user_id(), '__return_false' );
249        }
250        return $response;
251    }
252
253    /**
254     * Restore normal option reads after the offline module-list callback.
255     *
256     * @since 6.10.0
257     * @param mixed            $response Response after the callback.
258     * @param array            $handler  Route handler.
259     * @param \WP_REST_Request $request  REST request.
260     * @return mixed
261     */
262    public static function restore_module_options( $response, $handler, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- WordPress REST filter signature.
263        if ( '/jetpack/v4/module/all' === $request->get_route() ) {
264            remove_filter( 'default_option_monitor_receive_notifications', '__return_false' );
265            remove_filter( 'default_option_post_by_email_address' . get_current_user_id(), '__return_false' );
266        }
267        return $response;
268    }
269
270    /**
271     * Acts as a feature flag, returning a boolean for whether we should show the licensing UI.
272     *
273     * @since 1.2.0
274     *
275     * @return boolean
276     */
277    public static function is_licensing_ui_enabled() {
278        // Default changed to true in 1.5.0.
279        $is_enabled = true;
280
281        /*
282         * Bail if My Jetpack is not enabled,
283         * and thus the licensing UI shouldn't be enabled either.
284         */
285        if ( ! self::should_initialize() ) {
286            $is_enabled = false;
287        }
288
289        /**
290         * Acts as a feature flag, returning a boolean for whether we should show the licensing UI.
291         *
292         * @param bool $is_enabled Defaults to true.
293         *
294         * @since 1.2.0
295         * @since 1.5.0 Update default value to true.
296         */
297        return apply_filters(
298            'jetpack_my_jetpack_should_enable_add_license_screen',
299            $is_enabled
300        );
301    }
302
303    /**
304     * Add My Jetpack menu item to the admin menu.
305     *
306     * @return void
307     */
308    public static function add_my_jetpack_menu_item() {
309        if ( self::is_offline_features_enabled() && ! self::current_user_can_access_page() ) {
310            return;
311        }
312
313        $position    = defined( Admin_Menu::class . '::POSITION_FIRST' ) ? Admin_Menu::POSITION_FIRST : -10;
314        $menu_slug   = 'my-jetpack';
315        $page_suffix = Admin_Menu::add_menu(
316            __( 'My Jetpack', 'jetpack-my-jetpack' ),
317            __( 'My Jetpack', 'jetpack-my-jetpack' ),
318            'edit_posts',
319            $menu_slug,
320            array( __CLASS__, 'admin_page' ),
321            $position
322        );
323        add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
324        // Users who can edit posts but have no Jetpack menu get an admin_page_ hook instead.
325        add_action( 'load-admin_page_' . $menu_slug, array( __CLASS__, 'admin_init' ) );
326    }
327
328    /**
329     * Whether the current user may access the page selected by My Jetpack.
330     *
331     * @since 6.10.0
332     * @return bool
333     */
334    public static function current_user_can_access_page() {
335        return current_user_can( 'edit_posts' )
336            && ( ! self::is_offline_features_enabled() || REST_Main_Features::permissions_callback() );
337    }
338
339    /**
340     * Callback for the load my jetpack page hook.
341     *
342     * @return void
343     */
344    public static function admin_init() {
345        if ( self::is_offline_features_enabled() ) {
346            if ( ! self::current_user_can_access_page() ) {
347                wp_die( esc_html__( 'Sorry, you are not allowed to access this page.', 'jetpack-my-jetpack' ), 403 );
348            }
349            add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
350            return;
351        }
352
353        self::maybe_redirect_to_onboarding();
354
355        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No nonce needed for redirect flow control
356        $step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : '';
357
358        // If the user reaches the onboarding page, add a class to the body
359        if ( $step === 'onboarding' ) {
360            add_filter( 'admin_body_class', array( __CLASS__, 'add_onboarding_admin_body_class' ) );
361        }
362
363        self::$site_info = self::get_site_info();
364        add_filter( 'identity_crisis_container_id', array( static::class, 'get_idc_container_id' ) );
365        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
366    }
367
368    /**
369     * Whether this user has already settled setup, one way or the other.
370     *
371     * Two separate facts. Finishing is site-wide: the wizard connects the site and
372     * switches modules on, so once it is done nobody needs the takeover again.
373     * Skipping is this person saying "not now", and it must not answer for the next
374     * admin who arrives at a site nobody has set up.
375     *
376     * @internal Not part of the package's public API.
377     *
378     * @return bool
379     */
380    public static function is_onboarding_settled() {
381        if ( get_option( self::ONBOARDING_COMPLETED_OPTION, false ) ) {
382            return true;
383        }
384
385        return (bool) get_user_option( self::ONBOARDING_DISMISSED_USER_OPTION );
386    }
387
388    /**
389     * Send the user into setup when they land on My Jetpack without one.
390     *
391     * Runs from My Jetpack's own page load, which nothing but a request for that page
392     * reaches and which fires before any output, so it needs no guards of its own.
393     *
394     * @return void
395     */
396    public static function maybe_redirect_to_onboarding() {
397        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No nonce needed for redirect flow control
398        $step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : '';
399
400        $wizard_enabled = self::is_onboarding_wizard_enabled();
401        $connection     = new Connection_Manager();
402
403        // Only the wizard connects a user, so only it may count a site-only connection as
404        // unfinished setup. The single screen can only register the site, so asking it for
405        // an owner leaves that site on a screen with no way back out.
406        $connection_done = $wizard_enabled ? $connection->has_connected_owner() : $connection->is_connected();
407
408        // Onboarding's only action is the connect request, which answers a user
409        // without `jetpack_connect` with a 403.
410        $redirect_args = self::get_onboarding_redirect_args(
411            $step,
412            $connection_done,
413            self::is_onboarding_available() && current_user_can( 'jetpack_connect' ),
414            $wizard_enabled,
415            // Only the wizard records a settled run, and only a registered site may act on one:
416            // My Jetpack requires the site connection that accepting the terms creates.
417            $wizard_enabled && $connection->is_connected() && self::is_onboarding_settled()
418        );
419
420        if ( null === $redirect_args ) {
421            return;
422        }
423
424        $location = wp_sanitize_redirect( add_query_arg( $redirect_args, admin_url( 'admin.php' ) ) );
425
426        // Remove wp_get_referer filter applied in `fix_redirect` method of `Jetpack_Admin` class
427        remove_filter( 'wp_redirect', 'wp_get_referer' );
428        wp_safe_redirect( $location );
429
430        exit( 0 );
431    }
432
433    /**
434     * Whether the My Jetpack onboarding flow is available on this site.
435     *
436     * WordPress.com Simple sites are connected by definition and don't manage their
437     * connection through My Jetpack, so the onboarding flow (which asks the user to
438     * connect) never applies there. A pending partner coupon brings its own connect screen.
439     *
440     * @internal Not part of the package's public API.
441     *
442     * @return bool
443     */
444    public static function is_onboarding_available() {
445        if ( self::is_offline_features_enabled() ) {
446            return false;
447        }
448        return ! ( new Status_Host() )->is_wpcom_simple() && null === self::get_partner_coupon_screen();
449    }
450
451    /**
452     * The partner coupon screen's data, when it should replace the dashboard.
453     *
454     * Coupons exist only with the Jetpack plugin, which supplies their products and images.
455     *
456     * @since 6.5.0
457     *
458     * @return array{coupon: array, assetBaseUrl: string}|null
459     */
460    public static function get_partner_coupon_screen() {
461        if (
462            self::is_offline_features_enabled()
463            || ! self::should_initialize()
464            || ! current_user_can( 'manage_options' )
465            || ! Jetpack_Constants::is_defined( 'JETPACK__PLUGIN_FILE' )
466        ) {
467            return null;
468        }
469
470        $coupon = Partner_Coupon::get_coupon();
471        if ( ! $coupon ) {
472            return null;
473        }
474
475        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only picks which screen renders.
476        if ( ! isset( $_GET['showCouponRedemption'] ) && ( new Connection_Manager() )->has_connected_owner() ) {
477            return null;
478        }
479
480        return array(
481            'coupon'       => $coupon,
482            'assetBaseUrl' => plugins_url( '', Jetpack_Constants::get_constant( 'JETPACK__PLUGIN_FILE' ) ),
483        );
484    }
485
486    /**
487     * Decide whether the current My Jetpack request should redirect, and where to.
488     *
489     * @internal Not part of the package's public API.
490     *
491     * @param string $step                 The current `step` query param.
492     * @param bool   $connection_done      Whether the connection the takeover asks for is in place:
493     *                                     a connected owner with the wizard, a connected site without it.
494     * @param bool   $onboarding_available Whether the onboarding flow is available on this site.
495     * @param bool   $wizard_enabled       Whether the takeover renders the setup wizard.
496     * @param bool   $is_settled           Whether this user has already finished or skipped setup.
497     * @return array|null Query args for the redirect, or null to stay on the current page.
498     */
499    public static function get_onboarding_redirect_args( $step, $connection_done, $onboarding_available, $wizard_enabled = false, $is_settled = false ) {
500        /*
501         * Only the automatic redirect is suppressed once setup is settled. Asking for
502         * the onboarding page by its own URL still works, so someone who skipped can
503         * go back to it, and so can a second admin the site-wide flag does not cover.
504         */
505        if ( $onboarding_available && ! $connection_done && ! $is_settled && $step !== 'onboarding' ) {
506            // The connection setup asks for is not in place, so setup is unfinished.
507            return array(
508                'page' => 'my-jetpack',
509                'step' => 'onboarding',
510            );
511        }
512
513        // The wizard connects mid-flow: the user authorizes on WordPress.com and is sent
514        // back here to finish setup, so arriving connected is not the same as finishing.
515        if ( $step === 'onboarding' && $connection_done && $onboarding_available && $wizard_enabled ) {
516            return null;
517        }
518
519        if ( $step === 'onboarding' && ( ! $onboarding_available || $connection_done ) ) {
520            // Nothing left to set up here, or onboarding does not apply on this site.
521            return array( 'page' => 'my-jetpack' );
522        }
523
524        return null;
525    }
526
527    /**
528     * Empty the notice channels while the takeover is on screen.
529     *
530     * The takeover hides `.notice`, `.error` and `.updated` in CSS, which only
531     * catches notices that use WordPress's own classes. Plenty do not, and a plugin
532     * announcing itself in its own markup lands on top of a full-screen flow that
533     * has no way to scroll past it.
534     *
535     * On `in_admin_header`, the last hook before `admin-header.php` prints notices,
536     * so a notice registered late is caught too.
537     *
538     * @return void
539     */
540    public static function silence_onboarding_notices() {
541        if ( ! self::is_onboarding_request() ) {
542            return;
543        }
544
545        remove_all_actions( 'admin_notices' );
546        remove_all_actions( 'all_admin_notices' );
547        remove_all_actions( 'network_admin_notices' );
548        remove_all_actions( 'user_admin_notices' );
549    }
550
551    /**
552     * Keep Just In Time Messages off the takeover.
553     *
554     * Emptying the notice channels above would also drop the JITM, since that is how
555     * it renders, but only after it has enqueued its script and stylesheet. This is
556     * the package's own opt-out and it runs before any of that.
557     *
558     * The screen id cannot answer this on its own: the takeover and the My Jetpack
559     * dashboard are the same screen, and only the query arg separates them.
560     *
561     * @param bool   $show      Whether to show JITMs on this screen.
562     * @param string $screen_id The current screen's id.
563     * @return bool
564     */
565    public static function hide_onboarding_jitms( $show, $screen_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Required by the filter signature.
566        return self::is_onboarding_request() ? false : $show;
567    }
568
569    /**
570     * Add a body class to the My Jetpack onboarding page.
571     * This class hides the WP Admin toolbar and the sidebar menu.
572     *
573     * @param string $classes The body classes.
574     * @return string The modified body classes.
575     */
576    public static function add_onboarding_admin_body_class( $classes ) {
577        $classes .= ' jetpack-admin-full-screen';
578        return $classes;
579    }
580
581    /**
582     * Returns whether we are in condition to track to use
583     * Analytics functionality like Tracks, MC, or GA.
584     */
585    public static function can_use_analytics() {
586        $status     = new Status();
587        $connection = new Connection_Manager();
588        $tracking   = new Tracking( 'jetpack', $connection );
589
590        return $tracking->should_enable_tracking( new Terms_Of_Service(), $status );
591    }
592
593    /**
594     * Whether the onboarding takeover renders the setup wizard.
595     *
596     * Self-hosted only. On WordPress.com the platform owns the connection and nobody chose
597     * to install Jetpack, so a screen inviting them to start with it and connect an account
598     * does not apply. `is_wpcom_platform()` covers Simple and WordPress.com on Atomic, and
599     * requires wpcomsh, so other Atomic-hosted products still count as self-hosted here.
600     *
601     * @since 6.11.0
602     *
603     * @return bool
604     */
605    public static function is_onboarding_wizard_enabled() {
606        if ( ( new Status_Host() )->is_wpcom_platform() ) {
607            return false;
608        }
609
610        if ( ! Feature_Flags::is_enabled( self::ONBOARDING_WIZARD_FEATURE_FLAG ) ) {
611            return false;
612        }
613
614        // The wizard registers the site at its own first step, which writes the marker
615        // below, so asking again on the way back from WordPress.com would drop someone
616        // out of a run already under way.
617        if ( ( new Connection_Manager() )->is_connected() ) {
618            return true;
619        }
620
621        return self::has_never_registered_jetpack();
622    }
623
624    /**
625     * Whether the Jetpack plugin has never registered this site.
626     *
627     * `unique_registrations` is written on `jetpack_site_registered` by the Jetpack plugin
628     * alone, so a site that has only ever run a standalone plugin still reads as new here.
629     *
630     * @internal Not part of the package's public API.
631     *
632     * @since 6.11.0
633     *
634     * @return bool
635     */
636    public static function has_never_registered_jetpack() {
637        return ! \Jetpack_Options::get_option( 'unique_registrations' );
638    }
639
640    /**
641     * The wizard's configuration, or null while the takeover stays on its single screen.
642     *
643     * @since 6.11.0
644     *
645     * @return array{exitUrl: string, dashboardUrl: string}|null
646     */
647    public static function get_onboarding_wizard_state() {
648        if ( ! self::is_onboarding_wizard_enabled() ) {
649            return null;
650        }
651
652        return array(
653            // Where skipping the wizard lands: the admin menu is hidden, so leaving is always one click.
654            'exitUrl'      => admin_url( 'admin.php?page=my-jetpack' ),
655            // Where leaving Jetpack altogether lands: wp-admin's own dashboard.
656            'dashboardUrl' => admin_url(),
657        );
658    }
659
660    /**
661     * Whether the current request targets the My Jetpack admin page.
662     *
663     * @since 6.3.0
664     *
665     * @return bool
666     */
667    public static function is_my_jetpack_admin_request() {
668        if ( ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
669            return false;
670        }
671
672        return sanitize_text_field( wp_unslash( $_GET['page'] ) ) === 'my-jetpack'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
673    }
674
675    /**
676     * Whether the current request is the full-viewport onboarding takeover.
677     *
678     * Onboarding hides all wp-admin chrome and never renders through wp-build. The page
679     * is part of the question: `in_admin_header` and the JITM filter run everywhere.
680     *
681     * @since 6.3.0
682     *
683     * @return bool
684     */
685    public static function is_onboarding_request() {
686        if ( ! self::is_my_jetpack_admin_request() ) {
687            return false;
688        }
689
690        if ( ! isset( $_GET['step'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
691            return false;
692        }
693
694        return sanitize_text_field( wp_unslash( $_GET['step'] ) ) === 'onboarding'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
695    }
696
697    /**
698     * Whether this request is the onboarding takeover rather than the dashboard.
699     *
700     * Loading wp-build, enqueueing scripts and rendering the page must all agree,
701     * so they share this one expression.
702     *
703     * @since 6.3.0
704     *
705     * @return bool
706     */
707    public static function is_onboarding_takeover() {
708        return self::is_onboarding_request() && self::is_onboarding_available();
709    }
710
711    /**
712     * Alias the screen ID to satisfy wp-build's generated enqueue check.
713     *
714     * @since 6.3.0
715     *
716     * @return void
717     */
718    public static function alias_screen_id_for_wp_build() {
719        $screen = get_current_screen();
720
721        if ( ! $screen ) {
722            return;
723        }
724
725        self::$wp_build_original_screen_id = $screen->id;
726        $screen->id                        = self::WP_BUILD_PAGE_ID;
727    }
728
729    /**
730     * Undo alias_screen_id_for_wp_build(), since JITM builds its message path from the screen ID.
731     *
732     * @since 6.3.0
733     *
734     * @return void
735     */
736    public static function restore_screen_id_after_wp_build() {
737        $screen = get_current_screen();
738
739        if ( ! $screen || null === self::$wp_build_original_screen_id ) {
740            return;
741        }
742
743        $screen->id                        = self::$wp_build_original_screen_id;
744        self::$wp_build_original_screen_id = null;
745    }
746
747    /**
748     * Whether this request should load wp-build at all.
749     *
750     * Also what keeps WP_Build_Polyfills from replacing core scripts on every other admin page.
751     *
752     * @since 6.3.0
753     *
754     * @return bool
755     */
756    public static function should_load_wp_build() {
757        return self::is_my_jetpack_admin_request() && ! self::is_onboarding_takeover();
758    }
759
760    /**
761     * Whether this request renders the dashboard through wp-build.
762     *
763     * The generated render function is missing where the package was never built.
764     *
765     * @since 6.3.0
766     *
767     * @return bool
768     */
769    public static function should_render_wp_build() {
770        return ! self::is_onboarding_takeover()
771            && function_exists( 'jetpack_my_jetpack_my_jetpack_dashboard_wp_admin_render_page' );
772    }
773
774    /**
775     * Load wp-build for the My Jetpack dashboard.
776     *
777     * @since 6.3.0
778     *
779     * @return void
780     */
781    public static function maybe_load_wp_build() {
782        if ( ! self::should_load_wp_build() ) {
783            return;
784        }
785
786        $build_index = dirname( __DIR__ ) . '/build/build.php';
787
788        if ( file_exists( $build_index ) ) {
789            self::load_wp_build( $build_index );
790        }
791    }
792
793    /**
794     * Require the generated wp-build index and wire it into this request.
795     *
796     * @since 6.3.0
797     *
798     * @param string $build_index Path to the generated `build.php`.
799     * @return void
800     */
801    public static function load_wp_build( $build_index ) {
802        self::require_wp_build_with_screen_alias( $build_index );
803
804        // wp-build hooks module registration to wp_default_scripts, which has
805        // already fired by admin_menu â€” call it directly or the init module
806        // never reaches the import map.
807        if ( function_exists( 'jetpack_my_jetpack_register_script_modules' ) ) {
808            jetpack_my_jetpack_register_script_modules(); // @phan-suppress-current-line PhanUndeclaredFunction -- Checked with function_exists(); defined in the generated build/modules.php, which Phan excludes.
809        }
810
811        WP_Build_Polyfills::register(
812            'my-jetpack',
813            array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS )
814        );
815    }
816
817    /**
818     * Require the generated build file with the screen ID aliased across its enqueue check.
819     *
820     * @see WP_Build_Screen_Id::load_with_alias()
821     * @param string $build_index Path to the generated `build.php`.
822     * @return void
823     */
824    private static function require_wp_build_with_screen_alias( $build_index ) {
825        // Fallback: an older wp-build-polyfills under the jetpack-autoloader may predate load_with_alias().
826        if ( method_exists( WP_Build_Screen_Id::class, 'load_with_alias' ) ) {
827            WP_Build_Screen_Id::load_with_alias(
828                array( __CLASS__, 'alias_screen_id_for_wp_build' ),
829                array( __CLASS__, 'restore_screen_id_after_wp_build' ),
830                function () use ( $build_index ) {
831                    require_once $build_index;
832                }
833            );
834            return;
835        }
836
837        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
838        require_once $build_index;
839        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id_after_wp_build' ) );
840    }
841
842    /**
843     * Enqueue admin page assets.
844     *
845     * @return void
846     */
847    public static function enqueue_scripts() {
848        if ( self::is_offline_features_enabled() ) {
849            $data_handle = self::DATA_SCRIPT_HANDLE;
850            wp_register_script( $data_handle, false, array(), self::PACKAGE_VERSION, true );
851            wp_enqueue_script( $data_handle );
852            add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'add_script_data' ) );
853            wp_localize_script(
854                $data_handle,
855                'myJetpackInitialState',
856                array(
857                    'isOfflineFeatures'     => true,
858                    'products'              => array( 'items' => array() ),
859                    'adminUrl'              => esc_url( admin_url() ),
860                    'assetsUrl'             => self::get_assets_url(),
861                    'isJetpackPluginActive' => class_exists( 'Jetpack' ),
862                    'hiddenFeatures'        => Feature_Visibility::get_hidden(),
863                    'deprecatedModules'     => Feature_Visibility::get_deprecated_modules(),
864                    'myJetpackFlags'        => self::get_my_jetpack_flags(),
865                )
866            );
867            wp_localize_script(
868                $data_handle,
869                'myJetpackRest',
870                array(
871                    'apiRoot'  => esc_url_raw( rest_url() ),
872                    'apiNonce' => wp_create_nonce( 'wp_rest' ),
873                )
874            );
875            if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
876                wp_enqueue_script( 'wp-jp-i18n-loader' );
877            }
878            self::maybe_enqueue_header_controls_script();
879            return;
880        }
881
882        /**
883         * Fires after the My Jetpack page is initialized.
884         * Allows for enqueuing additional scripts only on the My Jetpack page.
885         *
886         * @since 4.35.7
887         */
888        do_action( 'myjetpack_enqueue_scripts' );
889        add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'add_script_data' ) );
890
891        // Script-less: it only prints the state below, before the dashboard's boot runs on DOMContentLoaded.
892        $data_handle = self::DATA_SCRIPT_HANDLE;
893        wp_register_script( $data_handle, false, array(), self::PACKAGE_VERSION, true );
894        wp_enqueue_script( $data_handle );
895        self::maybe_enqueue_header_controls_script();
896
897        if ( self::is_my_jetpack_admin_request() && self::is_onboarding_takeover() ) {
898            Assets::register_script(
899                self::ONBOARDING_SCRIPT_HANDLE,
900                '../build/onboarding.js',
901                __FILE__,
902                array(
903                    'dependencies' => array( $data_handle ),
904                    'enqueue'      => true,
905                    'in_footer'    => true,
906                    'textdomain'   => 'jetpack-my-jetpack',
907                )
908            );
909        } elseif ( self::should_render_wp_build() && wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
910            // Registered on every admin page but only enqueued when depended on; the esbuild bundles don't pull it in.
911            wp_enqueue_script( 'wp-jp-i18n-loader' );
912        }
913
914        $modules             = new Modules();
915        $connection          = new Connection_Manager();
916        $speed_score_history = new Speed_Score_History( get_site_url() );
917        $latest_score        = $speed_score_history->latest();
918        $previous_score      = array();
919        if ( $speed_score_history->count() > 1 ) {
920            $previous_score = $speed_score_history->latest( 1 );
921        }
922        $latest_score['previousScores'] = $previous_score['scores'] ?? array();
923
924        $sandboxed_domain = '';
925        $is_dev_version   = false;
926        if ( class_exists( 'Jetpack' ) ) {
927            $is_dev_version   = Jetpack::is_development_version();
928            $sandboxed_domain = defined( 'JETPACK__SANDBOX_DOMAIN' ) ? JETPACK__SANDBOX_DOMAIN : '';
929        }
930
931        wp_localize_script(
932            $data_handle,
933            'myJetpackInitialState',
934            array(
935                'products'               => array(
936                    'items' => Products::get_products(),
937                ),
938                'mainFeatures'           => Main_Features::get_state(),
939                'featuresBanner'         => array( 'isDismissed' => REST_Main_Features::is_banner_dismissed() ),
940                'onboardingWizard'       => self::get_onboarding_wizard_state(),
941                'header'                 => self::get_header_state(),
942                'plugins'                => Plugins_Installer::get_plugins(),
943                'themes'                 => Sync_Functions::get_themes(),
944                'myJetpackUrl'           => admin_url( 'admin.php?page=my-jetpack' ),
945                'myJetpackCheckoutUri'   => admin_url( 'admin.php?page=my-jetpack' ),
946                'topJetpackMenuItemUrl'  => Admin_Menu::get_top_level_menu_item_url(),
947                'siteSuffix'             => ( new Status() )->get_site_suffix(),
948                'siteUrl'                => esc_url( get_site_url() ),
949                'blogID'                 => Connection_Manager::get_site_id( true ),
950                'myJetpackVersion'       => self::PACKAGE_VERSION,
951                'myJetpackFlags'         => self::get_my_jetpack_flags(),
952                'fileSystemWriteAccess'  => self::has_file_system_write_access(),
953                'loadAddLicenseScreen'   => self::is_licensing_ui_enabled(),
954                'adminUrl'               => esc_url( admin_url() ),
955                'assetsUrl'              => self::get_assets_url(),
956                'IDCContainerID'         => static::get_idc_container_id(),
957                'userIsAdmin'            => current_user_can( 'manage_options' ),
958                'lifecycleStats'         => array(
959                    'jetpackPlugins'            => self::get_installed_jetpack_plugins(),
960                    'historicallyActiveModules' => \Jetpack_Options::get_option( 'historically_active_modules', array() ),
961                    'brokenModules'             => Red_Bubble_Notifications::check_for_broken_modules(),
962                    'isSiteConnected'           => $connection->is_connected(),
963                    'isUserConnected'           => $connection->is_user_connected(),
964                    'modules'                   => self::get_active_modules(),
965                ),
966                'recommendedModules'     => array(
967                    'modules'    => self::get_recommended_modules(),
968                    'isFirstRun' => \Jetpack_Options::get_option( 'recommendations_first_run', true ),
969                    'dismissed'  => \Jetpack_Options::get_option( 'dismissed_recommendations', false ),
970                ),
971                'isStatsModuleActive'    => $modules->is_active( 'stats' ),
972                'canUserViewStats'       => current_user_can( 'manage_options' ) || current_user_can( 'view_stats' ),
973                'hiddenFeatures'         => Feature_Visibility::get_hidden(),
974                'deprecatedModules'      => Feature_Visibility::get_deprecated_modules(),
975                'sandboxedDomain'        => $sandboxed_domain,
976                'isDevVersion'           => $is_dev_version,
977                'isAtomic'               => ( new Status_Host() )->is_woa_site(),
978                'isJetpackPluginActive'  => class_exists( 'Jetpack' ),
979                'latestBoostSpeedScores' => $latest_score,
980                'seoOptIn'               => self::get_seo_opt_in_state(),
981                'protectPageModuleUrls'  => (object) Products\Protect::get_protect_page_module_urls(),
982                'partnerCoupon'          => self::get_partner_coupon_screen(),
983            )
984        );
985
986        wp_localize_script(
987            $data_handle,
988            'myJetpackRest',
989            array(
990                'apiRoot'  => esc_url_raw( rest_url() ),
991                'apiNonce' => wp_create_nonce( 'wp_rest' ),
992            )
993        );
994
995        // Connection Initial State.
996        Connection_Initial_State::render_script( $data_handle );
997
998        // Required for Analytics.
999        if ( self::can_use_analytics() ) {
1000            Tracking::register_tracks_functions_scripts( true );
1001        }
1002    }
1003
1004    /**
1005     * What the shared header needs beyond the rest of the state, so it renders without a request.
1006     *
1007     * @since 6.10.0
1008     *
1009     * @return array{connectorsUrl: string|null} The state.
1010     */
1011    public static function get_header_state() {
1012        return array(
1013            // The same probe the connection package uses: the core screen ships with WordPress 7.0.
1014            'connectorsUrl' => file_exists( ABSPATH . 'wp-admin/options-connectors.php' ) ? admin_url( 'options-connectors.php' ) : null,
1015        );
1016    }
1017
1018    /**
1019     * Add My Jetpack data to the unified script data object.
1020     *
1021     * @param array $data The script data.
1022     * @return array
1023     */
1024    public static function add_script_data( $data ) {
1025        $block_availability = ! self::is_offline_features_enabled() && class_exists( '\Jetpack_Gutenberg' )
1026            ? \Jetpack_Gutenberg::get_cached_availability()
1027            : array();
1028
1029        $data['myJetpack']['siteEditor'] = array(
1030            'isBlockTheme'            => function_exists( 'wp_is_block_theme' ) && wp_is_block_theme(),
1031            'isSharingBlockAvailable' => isset( $block_availability['sharing-buttons'] )
1032                && $block_availability['sharing-buttons']['available'],
1033            'isLikeBlockAvailable'    => isset( $block_availability['like'] )
1034                && $block_availability['like']['available'],
1035            'activeThemeStylesheet'   => get_stylesheet(),
1036        );
1037
1038        return $data;
1039    }
1040
1041    /**
1042     * Whether module management can use the REST API.
1043     *
1044     * @since 6.11.0
1045     * @return bool
1046     */
1047    public static function is_modules_rest_api_enabled() {
1048        /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
1049        $enabled = apply_filters( 'rest_enabled', true );
1050        $user    = wp_get_current_user();
1051        $auth    = false;
1052        // Passing true skips WordPress's nonce check, which would sign the user out; restore the user if another filter clears it.
1053        try {
1054            /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
1055            $auth = apply_filters( 'rest_authentication_errors', true );
1056        } finally {
1057            wp_set_current_user( $user->ID );
1058        }
1059        return $enabled && ! is_wp_error( $enabled ) && false !== $auth && ! is_wp_error( $auth );
1060    }
1061
1062    /**
1063     * Get a Features management link with a classic Modules fallback.
1064     *
1065     * @since 6.11.0
1066     * @param array     $args     Query arguments from a classic Modules screen link; only `s` and `module_tag` can carry over to Features.
1067     * @param bool|null $eligible Set to true when the URL points at the Features tab, false when it is the classic Modules screen.
1068     * @return string The management URL.
1069     */
1070    public static function get_modules_management_url( $args = array(), &$eligible = null ) {
1071        $eligible = false;
1072        /**
1073         * Filters whether links to the classic Modules screen may send people to the Features tab in My Jetpack instead.
1074         *
1075         * @since 6.11.0
1076         * @param bool $available True to allow the Features tab; false keeps the classic Modules screen, for a host where My Jetpack is not shown.
1077         */
1078        $available = apply_filters( 'jetpack_my_jetpack_modules_management_available', true );
1079        $available = $available && Feature_Flags::is_enabled( self::MODULES_TO_FEATURES_FLAG );
1080        $classic   = add_query_arg( array_merge( urlencode_deep( $args ), array( 'page' => 'jetpack_modules' ) ), admin_url( 'admin.php' ) );
1081        if (
1082            ! $available
1083            || array_diff( array_keys( $args ), array( 's', 'module_tag' ) )
1084            || array_filter( $args, 'is_string' ) !== $args
1085            || ! Plugins_Installer::is_plugin_active( 'jetpack/jetpack.php' )
1086            || ! current_user_can( 'jetpack_manage_modules' )
1087            || ! current_user_can( 'manage_options' )
1088            || ! self::current_user_can_access_page()
1089            || ( new Status() )->is_offline_mode()
1090            || ( new Status_Host() )->is_wpcom_simple()
1091            || is_network_admin()
1092            || ! self::should_initialize()
1093            || ! ( new Connection_Manager() )->is_connected()
1094            || null !== self::get_partner_coupon_screen()
1095            || ! self::is_modules_rest_api_enabled()
1096        ) {
1097            return $classic;
1098        }
1099
1100        if ( isset( $args['module_tag'] ) && '' !== $args['module_tag'] ) {
1101            if ( ! function_exists( 'jetpack_get_module_i18n_tag' ) ) {
1102                return $classic;
1103            }
1104            $known      = array();
1105            $modules    = new Modules();
1106            $hidden     = Main_Features::get_hidden_modules();
1107            $deprecated = Feature_Visibility::get_deprecated_modules();
1108            foreach ( $modules->get_available() as $slug ) {
1109                if ( in_array( $slug, $hidden, true ) || ( in_array( $slug, $deprecated, true ) && ! $modules->is_active( $slug ) ) ) {
1110                    continue;
1111                }
1112                $module = $modules->get( $slug );
1113                $known  = array_merge( $known, $module['module_tags'] ?? array() );
1114            }
1115            $known = array_map( array( Jetpack::class, 'translate_module_tag' ), $known );
1116            if ( ! in_array( $args['module_tag'], $known, true ) ) {
1117                return $classic;
1118            }
1119        }
1120
1121        if ( isset( $args['s'] ) && '' !== $args['s'] ) {
1122            require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php';
1123            $hidden        = Main_Features::get_hidden_modules();
1124            $deprecated    = Feature_Visibility::get_deprecated_modules();
1125            $hidden_match  = false;
1126            $visible_match = false;
1127            foreach ( Jetpack::get_translated_modules( \Jetpack_Admin::init()->get_modules() ) as $slug => $module ) {
1128                if ( ! empty( $args['module_tag'] ) && ! in_array( $args['module_tag'], $module['module_tags'], true ) ) {
1129                    continue;
1130                }
1131                $text  = $module['name'] . ' ' . $module['description'] . ' ' . $module['long_description'] . ' ' . $module['search_terms'] . ' ' . implode( ',', $module['module_tags'] );
1132                $match = function_exists( 'mb_stripos' ) ? mb_stripos( $text, $args['s'] ) : stripos( $text, $args['s'] );
1133                if ( false === $match ) {
1134                    continue;
1135                }
1136                if ( in_array( $slug, $hidden, true ) || ( in_array( $slug, $deprecated, true ) && ! $module['activated'] ) ) {
1137                    $hidden_match = true;
1138                } else {
1139                    $visible_match = true;
1140                    break;
1141                }
1142            }
1143            if ( $hidden_match && ! $visible_match ) {
1144                return $classic;
1145            }
1146        }
1147
1148        $eligible = true;
1149        $hash     = array();
1150        if ( isset( $args['s'] ) && '' !== $args['s'] ) {
1151            $hash['search'] = $args['s'];
1152        }
1153        if ( isset( $args['module_tag'] ) && '' !== $args['module_tag'] ) {
1154            $hash['module_tag'] = $args['module_tag'];
1155        }
1156        return add_query_arg(
1157            array_merge(
1158                urlencode_deep( $args ),
1159                array(
1160                    'page'             => 'my-jetpack',
1161                    'modules_fallback' => '1',
1162                )
1163            ),
1164            admin_url( 'admin.php' )
1165        )
1166            . '#/features' . ( $hash ? '?' . http_build_query( $hash, '', '&', PHP_QUERY_RFC3986 ) : '' );
1167    }
1168
1169    /**
1170     * On a My Jetpack page reached from a redirected Modules link (`modules_fallback=1`), send the browser back to the classic Modules screen:
1171     * always when JavaScript is off, and for everyone when the REST API is off or blocked.
1172     *
1173     * @since 6.11.0
1174     * @return void
1175     */
1176    public static function print_modules_fallback() {
1177        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only navigation fallback.
1178        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Only the exact allowlisted marker is accepted.
1179        if ( 'my-jetpack' !== ( $_GET['page'] ?? null ) || '1' !== ( $_GET['modules_fallback'] ?? null ) || ! Plugins_Installer::is_plugin_active( 'jetpack/jetpack.php' ) || ! current_user_can( 'jetpack_manage_modules' ) ) {
1180            return;
1181        }
1182        $args = array( 'page' => 'jetpack_modules' );
1183        foreach ( array( 's', 'module_tag' ) as $key ) {
1184            if ( isset( $_GET[ $key ] ) && is_string( $_GET[ $key ] ) ) {
1185                $args[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $key ] ) );
1186            }
1187        }
1188        // phpcs:enable WordPress.Security.NonceVerification.Recommended
1189        $meta = '<meta http-equiv="refresh" content="0; url=' . esc_url( add_query_arg( urlencode_deep( $args ), admin_url( 'admin.php' ) ) ) . '">';
1190        echo '<noscript>' . $meta . '</noscript>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- URL escaped above.
1191        if ( ! self::is_modules_rest_api_enabled() ) {
1192            echo $meta; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- URL escaped above.
1193        }
1194    }
1195
1196    /**
1197     * Get the slug and label of the Features tab, for footer links to it.
1198     *
1199     * Kept for footers built before the Products tab was removed: Jetpack 16.3 calls this, and
1200     * older JS footers read it from script data. Without it they link to "Products".
1201     *
1202     * @since 6.4.0
1203     *
1204     * @return array{slug: string, label: string}
1205     */
1206    public static function get_products_section() {
1207        return array(
1208            'slug'  => 'features',
1209            'label' => _x( 'Features', 'Navigation item', 'jetpack-my-jetpack' ),
1210        );
1211    }
1212
1213    /**
1214     * Add My Jetpack's availability, image base URL and link targets to admin script data.
1215     *
1216     * Printed on every admin page by Script_Data, so the connection screen can resolve its
1217     * illustrations and Jetpack footers know whether to link to My Jetpack.
1218     *
1219     * @since 6.3.0
1220     *
1221     * @param array $data Script data.
1222     * @return array
1223     */
1224    public static function add_admin_script_data( $data ) {
1225        $data['myJetpack']['isAvailable']     = self::is_admin_page_available();
1226        $data['myJetpack']['assetsUrl']       = self::get_assets_url();
1227        $data['myJetpack']['productsSection'] = self::get_products_section();
1228        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only selects which navigation data to seed.
1229        if ( in_array( $_GET['page'] ?? '', array( 'my-jetpack', 'jetpack-settings', 'jetpack' ), true ) ) {
1230            $data['myJetpack']['modulesManagementUrl'] = self::get_modules_management_url();
1231        }
1232
1233        // Only administrators can open the pages this points at, so spare everyone else the lookup.
1234        if ( current_user_can( 'manage_options' ) ) {
1235            $data['myJetpack']['aiManageUrl'] = esc_url_raw( Products\Jetpack_Ai::get_manage_url() );
1236        }
1237
1238        if (
1239            self::is_my_jetpack_admin_request()
1240            && self::is_offline_features_enabled()
1241            && self::should_initialize()
1242            && REST_Main_Features::permissions_callback()
1243        ) {
1244            $data['myJetpack']['offlineFeatures'] = array(
1245                'mainFeatures' => Main_Features::get_state( true ),
1246                'plugins'      => Plugins_Installer::get_plugins(),
1247            );
1248            $data['myJetpack']['offlineFeatures']['mainFeatures']['available_admin_pages'] = self::get_registered_admin_page_slugs();
1249        }
1250
1251        return $data;
1252    }
1253
1254    /**
1255     * Get the slugs of registered admin pages.
1256     *
1257     * Skip hooks containing `?`: they are `edit.php?post_type=...` style pages, not page slugs.
1258     *
1259     * @return string[]
1260     */
1261    private static function get_registered_admin_page_slugs() {
1262        global $_registered_pages;
1263
1264        $hooks = array_filter(
1265            array_keys( $_registered_pages ?? array() ),
1266            static fn( $hook ) => false !== strpos( $hook, '_page_' ) && false === strpos( $hook, '?' )
1267        );
1268
1269        return array_values(
1270            array_unique(
1271                array_map( static fn( $hook ) => substr( $hook, strpos( $hook, '_page_' ) + 6 ), $hooks )
1272            )
1273        );
1274    }
1275
1276    /**
1277     * Whether My Jetpack's admin page is available to the current user.
1278     *
1279     * Meaningful only after admin_menu has registered the page.
1280     *
1281     * @since 6.5.0
1282     *
1283     * @return bool
1284     */
1285    public static function is_admin_page_available() {
1286        global $_registered_pages;
1287
1288        return did_action( 'my_jetpack_init' ) > 0
1289            && isset( $_registered_pages[ get_plugin_page_hookname( 'my-jetpack', 'jetpack' ) ] )
1290            && current_user_can( 'edit_posts' );
1291    }
1292
1293    /**
1294     * Get the base URL of the package's built images, with a trailing slash.
1295     *
1296     * @since 6.3.0
1297     *
1298     * @return string
1299     */
1300    public static function get_assets_url() {
1301        return trailingslashit( Assets::normalize_path( plugins_url( '../build/images/', __FILE__ ) ) );
1302    }
1303
1304    /**
1305     * Get installed Jetpack plugins
1306     *
1307     * @return array
1308     */
1309    public static function get_installed_jetpack_plugins() {
1310        $plugin_slugs = array_keys( Plugins_Installer::get_plugins() );
1311        $plugin_slugs = array_map(
1312            static function ( $slug ) {
1313                $parts = explode( '/', $slug );
1314                // Return the last segment of the filepath without the PHP extension
1315                return str_replace( '.php', '', $parts[ count( $parts ) - 1 ] );
1316            },
1317            $plugin_slugs
1318        );
1319
1320        return array_values( array_intersect( self::JETPACK_PLUGIN_SLUGS, $plugin_slugs ) );
1321    }
1322
1323    /**
1324     * Get active modules (except ones enabled by default)
1325     *
1326     * @return array
1327     */
1328    public static function get_active_modules() {
1329        $modules        = new Modules();
1330        $active_modules = $modules->get_active();
1331
1332        // if the Jetpack plugin is active, filter out the modules that are active by default
1333        if ( class_exists( 'Jetpack' ) && ! empty( $active_modules ) ) {
1334            $active_modules = array_diff( $active_modules, Jetpack::get_default_modules() );
1335        }
1336        return array_values( $active_modules );
1337    }
1338
1339    /**
1340     * Determine if the current user is "new" to Jetpack
1341     * This is used to vary some messaging in My Jetpack
1342     *
1343     * On the front-end, purchases are also taken into account
1344     *
1345     * @return bool
1346     */
1347    public static function is_jetpack_user_new() {
1348        // is the user connected?
1349        $connection = new Connection_Manager();
1350        if ( $connection->is_user_connected() ) {
1351            return false;
1352        }
1353
1354        // TODO: add a data point for the last known connection/ disconnection time
1355
1356        // are any modules active?
1357        $active_modules = self::get_active_modules();
1358        if ( ! empty( $active_modules ) ) {
1359            return false;
1360        }
1361
1362        // check for other Jetpack plugins that are installed on the site (active or not)
1363        // If there's more than one Jetpack plugin active, this user is not "new"
1364        $plugin_slugs              = array_keys( Plugins_Installer::get_plugins() );
1365        $plugin_slugs              = array_map(
1366            static function ( $slug ) {
1367                $parts = explode( '/', $slug );
1368                // Return the last segment of the filepath without the PHP extension
1369                return str_replace( '.php', '', $parts[ count( $parts ) - 1 ] );
1370            },
1371            $plugin_slugs
1372        );
1373        $installed_jetpack_plugins = array_intersect( self::JETPACK_PLUGIN_SLUGS, $plugin_slugs );
1374        if ( is_countable( $installed_jetpack_plugins ) && count( $installed_jetpack_plugins ) >= 2 ) {
1375            return false;
1376        }
1377
1378        // Does the site have any purchases?
1379        $purchases = Wpcom_Products::get_site_current_purchases();
1380        if ( ! empty( $purchases ) && ! is_wp_error( $purchases ) ) {
1381            return false;
1382        }
1383
1384        return true;
1385    }
1386
1387    /**
1388     * Register My Jetpack's feature flags.
1389     *
1390     * @since 6.11.0
1391     *
1392     * @return void
1393     */
1394    public static function register_feature_flags() {
1395        Feature_Flags::register(
1396            self::HEADER_CONTROLS_FLAG,
1397            array(
1398                'default'     => false,
1399                'description' => 'Show the global controls in the shared header of My Jetpack and Boost.',
1400                'owner'       => 'my-jetpack',
1401            )
1402        );
1403        Feature_Flags::register(
1404            self::OVERVIEW_V1_FLAG,
1405            array(
1406                'default'     => false,
1407                'description' => 'Show Overview v1 in My Jetpack.',
1408                'owner'       => 'my-jetpack',
1409            )
1410        );
1411        Feature_Flags::register(
1412            self::MODULES_TO_FEATURES_FLAG,
1413            array(
1414                'default'     => false,
1415                'description' => 'Open supported classic Modules links in Features.',
1416                'owner'       => 'my-jetpack',
1417            )
1418        );
1419    }
1420
1421    /**
1422     * Whether the shared header shows its controls: the flag is on, My Jetpack runs, and not in offline Features.
1423     *
1424     * Boost's dashboard gets My Jetpack's page state even where My Jetpack is off, hence the second check.
1425     * Offline Features prints no connection state for the controls to read, and has no connection to show.
1426     *
1427     * @return bool
1428     */
1429    private static function is_header_controls_enabled() {
1430        return Feature_Flags::is_enabled( self::HEADER_CONTROLS_FLAG )
1431            && self::should_initialize()
1432            && ! self::is_offline_features_enabled();
1433    }
1434
1435    /**
1436     * Give dashboards outside My Jetpack, such as Boost's, the header's controls as a shared script.
1437     *
1438     * Bundling them instead would ship a second copy of the connection package next to the page's own.
1439     *
1440     * @return void
1441     */
1442    private static function maybe_enqueue_header_controls_script() {
1443        if ( self::is_my_jetpack_admin_request() || ! self::is_header_controls_enabled() ) {
1444            return;
1445        }
1446
1447        Assets::register_script(
1448            self::HEADER_CONTROLS_SCRIPT_HANDLE,
1449            '../build/header-controls.js',
1450            __FILE__,
1451            array(
1452                'dependencies' => array( self::DATA_SCRIPT_HANDLE ),
1453                'enqueue'      => true,
1454                'in_footer'    => true,
1455                'textdomain'   => 'jetpack-my-jetpack',
1456            )
1457        );
1458    }
1459
1460    /**
1461     *  Build flags for My Jetpack UI
1462     *
1463     *  @return array
1464     */
1465    public static function get_my_jetpack_flags() {
1466        $flags = array(
1467            'videoPressStats'          => Jetpack_Constants::is_true( 'JETPACK_MY_JETPACK_VIDEOPRESS_STATS_ENABLED' ),
1468            'showFullJetpackStatsCard' => class_exists( 'Jetpack' ) || Products\Stats::is_standalone_plugin_active(),
1469            // Only says which destination `manage_url` is: the legacy Stats page
1470            // caches its report and wants a `force_refresh` hint the dashboard does not.
1471            'premiumAnalyticsEnabled'  => Products\Stats::is_premium_analytics_enabled(),
1472            'showAiModuleToggle'       => Products\Jetpack_Ai::is_feature_ui_enabled(),
1473            'headerControls'           => self::is_header_controls_enabled(),
1474            'overviewV1'               => self::is_overview_v1_enabled(),
1475        );
1476
1477        return $flags;
1478    }
1479
1480    /**
1481     * Whether My Jetpack shows Overview v1.
1482     *
1483     * @since 6.10.0
1484     * @return bool
1485     */
1486    public static function is_overview_v1_enabled() {
1487        return Feature_Flags::is_enabled( self::OVERVIEW_V1_FLAG );
1488    }
1489
1490    /**
1491     * Build the state the My Jetpack "try the new SEO experience" opt-in card hydrates from.
1492     *
1493     * The card invites an existing self-hosted install to switch over to the new Jetpack SEO
1494     * dashboard (JETPACK-1700). Gating lives server-side, where the signals actually are. The
1495     * card shows only when all of:
1496     *
1497     * - the new SEO product is available â€” the `rsm_jetpack_seo` feature filter is on (the SEO
1498     *   package autoloads regardless, so `class_exists()` alone isn't enough; the filter is the
1499     *   real availability switch and the same one the SEO package gates its own surface behind);
1500     * - the site is self-hosted â€” WordPress.com (Simple + Atomic) decides its own SEO surface, so
1501     *   the opt-in card is for self-hosted installs only;
1502     * - the install hasn't opted in yet â€” the SEO surface isn't visible. On wpcom
1503     *   the SEO package's `is_seo_surface_visible()` short-circuits to `true`, so the
1504     *   "not visible yet" check also doubles as the self-hosted guard, but we check the platform
1505     *   explicitly for clarity.
1506     *
1507     * Referenced through `class_exists()` rather than a hard composer dependency: both packages
1508     * ship inside the Jetpack plugin and the SEO surface is feature-flagged, so a guarded read of
1509     * its public API keeps this from adding plumbing to consumers that don't load SEO.
1510     *
1511     * The on-success destination is computed by the opt-in endpoint itself; we only seed the card
1512     * with the same admin URL so the button has a sensible fallback before the request resolves.
1513     *
1514     * @return array{showCard: bool, redirect: string}
1515     */
1516    public static function get_seo_opt_in_state() {
1517        // Guard with method_exists rather than class_exists: an older bundled SEO package can ship
1518        // the Initializer class without this method, and class_exists alone would still fatal.
1519        $seo_initializer = 'Automattic\Jetpack\SEO\Initializer';
1520        // @phan-suppress-next-line PhanUndeclaredClassReference -- optional SEO package, guarded by method_exists.
1521        $show_card = method_exists( $seo_initializer, 'is_optin_available' ) && $seo_initializer::is_optin_available();
1522
1523        return array(
1524            'showCard' => $show_card,
1525            'redirect' => admin_url( 'admin.php?page=jetpack-seo' ),
1526        );
1527    }
1528
1529    /**
1530     * Echoes the admin page content.
1531     *
1532     * @return void
1533     */
1534    public static function admin_page() {
1535        // Asked again here rather than inferred from the redirect: with the wizard on a
1536        // connected user is deliberately left on this screen to finish setup, and anyone
1537        // who cannot connect never reaches it.
1538        if ( self::is_onboarding_takeover() ) {
1539            echo '<div id="my-jetpack-container"></div>';
1540            return;
1541        }
1542
1543        if ( self::should_render_wp_build() ) {
1544            jetpack_my_jetpack_my_jetpack_dashboard_wp_admin_render_page(); // @phan-suppress-current-line PhanUndeclaredFunction -- should_render_wp_build() checks function_exists(); defined in the generated build/pages/, which Phan excludes.
1545        }
1546    }
1547
1548    /**
1549     * Register the REST API routes.
1550     *
1551     * @return void
1552     */
1553    public static function register_rest_endpoints() {
1554        if ( self::is_offline_features_enabled() ) {
1555            ( new REST_Main_Features() )->register_rest_routes();
1556            return;
1557        }
1558
1559        new REST_Products();
1560        new REST_Purchases();
1561        ( new REST_Jetpack_AI_JWT() )->register_rest_route();
1562        new REST_Recommendations_Evaluation();
1563
1564        ( new REST_Onboarding() )->register_rest_routes();
1565        ( new REST_Main_Features() )->register_rest_routes();
1566
1567        Products::register_product_endpoints();
1568        Historically_Active_Modules::register_rest_endpoints();
1569        Jetpack_Manage::register_rest_endpoints();
1570        Red_Bubble_Notifications::register_rest_endpoints();
1571
1572        register_rest_route(
1573            'my-jetpack/v1',
1574            'site',
1575            array(
1576                'methods'             => \WP_REST_Server::READABLE,
1577                'callback'            => __CLASS__ . '::get_site',
1578                'permission_callback' => __CLASS__ . '::permissions_callback',
1579            )
1580        );
1581
1582        register_rest_route(
1583            'my-jetpack/v1',
1584            'site/dismiss-welcome-banner',
1585            array(
1586                'methods'             => \WP_REST_Server::EDITABLE,
1587                'callback'            => __CLASS__ . '::dismiss_welcome_banner',
1588                'permission_callback' => __CLASS__ . '::permissions_callback',
1589            )
1590        );
1591    }
1592
1593    /**
1594     * Check user capability to access the endpoint.
1595     *
1596     * @access public
1597     * @static
1598     *
1599     * @return true|WP_Error
1600     */
1601    public static function permissions_callback() {
1602        return current_user_can( 'manage_options' );
1603    }
1604
1605    /**
1606     * Return true if we should initialize the My Jetpack admin page.
1607     */
1608    public static function should_initialize() {
1609        $should = true;
1610
1611        if ( ( new Status() )->is_offline_mode() ) {
1612            $should = self::is_offline_features_enabled();
1613        }
1614
1615        /**
1616         * Allows filtering whether My Jetpack should be initialized.
1617         * Outside REST requests, once the user is set, this cannot enable offline Features for a user without access.
1618         *
1619         * @since 0.5.0-alpha
1620         *
1621         * @param bool $shoud_initialize Should we initialize My Jetpack?
1622         */
1623        $should = apply_filters( 'jetpack_my_jetpack_should_initialize', $should );
1624
1625        // REST authenticates after route registration; its permission callbacks check access.
1626        if ( self::is_offline_features_enabled() && ! Jetpack_Constants::is_true( 'REST_REQUEST' ) && function_exists( 'wp_get_current_user' ) && did_action( 'set_current_user' ) && ! self::current_user_can_access_page() ) {
1627            return false;
1628        }
1629
1630        return $should;
1631    }
1632
1633    /**
1634     * Whether the site uses the local Features entry instead of the connected dashboard.
1635     *
1636     * @since 6.10.0
1637     * @return bool
1638     */
1639    public static function is_offline_features_enabled() {
1640        if ( ! ( new Status() )->is_offline_mode() ) {
1641            return false;
1642        }
1643
1644        /**
1645         * Enable local Features data and entry in offline mode.
1646         *
1647         * @since 6.10.0
1648         * @param bool $enabled Whether to enable offline Features. Default false.
1649         */
1650        return (bool) apply_filters( 'jetpack_my_jetpack_offline_features', false );
1651    }
1652
1653    /**
1654     * Hook into several connection-based actions to update the historically active Jetpack modules
1655     * If the transient that indicates the list needs to be synced, update it and delete the transient
1656     *
1657     * @return void
1658     */
1659    public static function setup_historically_active_jetpack_modules_sync() {
1660        // yummmm. ham.
1661        $ham = new Historically_Active_Modules();
1662        if ( get_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY ) && ! wp_doing_ajax() ) {
1663            $ham::update_historically_active_jetpack_modules();
1664            delete_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY );
1665        }
1666
1667        $actions = array(
1668            'jetpack_site_registered',
1669            'jetpack_user_authorized',
1670            'activated_plugin',
1671        );
1672
1673        foreach ( $actions as $action ) {
1674            add_action( $action, array( $ham, 'queue_historically_active_jetpack_modules_update' ), 5 );
1675        }
1676
1677        // Modules are often updated async, so we need to update them right away as there will sometimes be no page reload.
1678        add_action( 'jetpack_activate_module', array( $ham, 'update_historically_active_jetpack_modules' ), 5 );
1679    }
1680
1681    /**
1682     * Site full-data endpoint.
1683     *
1684     * @return object Site data.
1685     */
1686    public static function get_site() {
1687        $site_id           = \Jetpack_Options::get_option( 'id' );
1688        $wpcom_endpoint    = sprintf( '/sites/%d?force=wpcom', $site_id );
1689        $wpcom_api_version = '1.1';
1690        $response          = Client::wpcom_json_api_request_as_blog( $wpcom_endpoint, $wpcom_api_version );
1691        $response_code     = wp_remote_retrieve_response_code( $response );
1692        $body              = json_decode( wp_remote_retrieve_body( $response ) );
1693
1694        if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
1695            return new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ) );
1696        }
1697
1698        return rest_ensure_response( $body );
1699    }
1700
1701    /**
1702     * Populates the self::$site_info var with site data from the /sites/%d endpoint
1703     *
1704     * @return object|WP_Error
1705     */
1706    public static function get_site_info() {
1707        static $site_info = null;
1708
1709        if ( $site_info !== null ) {
1710            return $site_info;
1711        }
1712
1713        // Check for a cached value before doing lookup
1714        $stored_site_info = get_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY );
1715        if ( $stored_site_info !== false ) {
1716            return $stored_site_info;
1717        }
1718
1719        $response = self::get_site();
1720        if ( is_wp_error( $response ) ) {
1721            return $response;
1722        }
1723        $site_info = $response->data;
1724        set_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY, $site_info, DAY_IN_SECONDS );
1725
1726        return $site_info;
1727    }
1728
1729    /**
1730     * Returns whether a site has been determined "commercial" or not.
1731     *
1732     * @return bool|null
1733     */
1734    public static function is_commercial_site() {
1735        if ( is_wp_error( self::$site_info ) ) {
1736            return null;
1737        }
1738
1739        return empty( self::$site_info->options->is_commercial ) ? false : self::$site_info->options->is_commercial;
1740    }
1741
1742    /**
1743     * Check if site is registered (has been connected before).
1744     *
1745     * @return bool
1746     */
1747    public static function is_registered() {
1748        return (bool) \Jetpack_Options::get_option( 'id' );
1749    }
1750
1751    /**
1752     * Dismiss the welcome banner.
1753     *
1754     * @return \WP_REST_Response
1755     */
1756    public static function dismiss_welcome_banner() {
1757        \Jetpack_Options::update_option( 'dismissed_welcome_banner', true );
1758        return rest_ensure_response( array( 'success' => true ) );
1759    }
1760
1761    /**
1762     * Returns "yes" if the site has file write access to the plugins folder, "no" otherwise.
1763     *
1764     * @return string
1765     **/
1766    public static function has_file_system_write_access() {
1767
1768        $cache = get_transient( 'my_jetpack_write_access' );
1769
1770        if ( false !== $cache ) {
1771            return $cache;
1772        }
1773
1774        if ( ! function_exists( 'get_filesystem_method' ) ) {
1775            require_once ABSPATH . 'wp-admin/includes/file.php';
1776        }
1777
1778        require_once ABSPATH . 'wp-admin/includes/template.php';
1779
1780        $write_access = 'no';
1781
1782        $filesystem_method = get_filesystem_method( array(), WP_PLUGIN_DIR );
1783        if ( 'direct' === $filesystem_method ) {
1784            $write_access = 'yes';
1785        }
1786
1787        if ( 'no' === $write_access ) {
1788            ob_start();
1789            $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
1790            ob_end_clean();
1791
1792            if ( $filesystem_credentials_are_stored ) {
1793                $write_access = 'yes';
1794            }
1795        }
1796
1797        set_transient( 'my_jetpack_write_access', $write_access, 30 * MINUTE_IN_SECONDS );
1798
1799        return $write_access;
1800    }
1801
1802    /**
1803     * Get container IDC for the IDC screen.
1804     *
1805     * @return string
1806     */
1807    public static function get_idc_container_id() {
1808        return static::IDC_CONTAINER_ID;
1809    }
1810
1811    /**
1812     * Conditionally append the red bubble notification to the "Jetpack" menu item if there are alerts to show.
1813     *
1814     * On My Jetpack page: Uses blocking behavior to fetch fresh data.
1815     * On other admin pages: Uses cached data only to avoid blocking, with async JS fetch if cache is empty.
1816     *
1817     * @return void
1818     */
1819    public static function maybe_show_red_bubble() {
1820        global $pagenow;
1821
1822        // Don't show red bubble alerts for non-admin users
1823        // These alerts are generally only actionable for admins
1824        if ( ! current_user_can( 'manage_options' ) ) {
1825            return;
1826        }
1827
1828        // Don't show any red bubbles when Jetpack is disconnected
1829        // Users can't act on most alerts without a connection
1830        $connection = new Connection_Manager();
1831        if ( ! $connection->is_connected() ) {
1832            return;
1833        }
1834
1835        // Check if we're on the My Jetpack page.
1836        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1837        $page               = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
1838        $is_my_jetpack_page = $pagenow === 'admin.php' && $page === 'my-jetpack';
1839
1840        if ( $is_my_jetpack_page ) {
1841            // On My Jetpack page: use blocking behavior for fresh data.
1842            add_filter( 'my_jetpack_red_bubble_notification_slugs', array( Red_Bubble_Notifications::class, 'add_red_bubble_alerts' ) );
1843            $red_bubble_alerts = Red_Bubble_Notifications::get_red_bubble_alerts();
1844        } else {
1845            // On other pages: use cached data only to avoid blocking.
1846            $cached_alerts = Red_Bubble_Notifications::get_cached_alerts();
1847
1848            if ( false === $cached_alerts ) {
1849                // No cache: warm it asynchronously via JS. Register a hidden zero-count
1850                // placeholder so Menu_Renderer emits a `my-jetpack` badge element the
1851                // warmer can reveal (see async-notification-bubble.ts) without a reload.
1852                Menu_Badges::init(); // idempotent; wires the renderer.
1853                Notification_Counts::register(
1854                    'my-jetpack',
1855                    array(
1856                        'menu_slug' => 'my-jetpack',
1857                        'count'     => 0,
1858                        'type'      => 'count',
1859                    )
1860                );
1861                add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_red_bubble_script' ) );
1862                return;
1863            }
1864
1865            $red_bubble_alerts = $cached_alerts;
1866        }
1867
1868        // Filter out silent alerts.
1869        $red_bubble_alerts = array_filter(
1870            $red_bubble_alerts,
1871            function ( $alert ) {
1872                return empty( $alert['is_silent'] );
1873            }
1874        );
1875
1876        // Report each non-silent alert to the central menu-badges registry as an
1877        // attention entry (count 1). The registry + renderer own the badge.
1878        Menu_Badges::init(); // idempotent; wires the renderer.
1879
1880        // Connection errors are owned by the Error Handler (the single source of truth):
1881        // surface any it reports for this viewer as their own attention entry. Read live
1882        // rather than from the red-bubble transient, since that cache is not viewer-keyed.
1883        $has_connection_error = self::has_connection_error();
1884        if ( $has_connection_error ) {
1885            Notification_Counts::register(
1886                'my-jetpack-connection-error',
1887                array(
1888                    'menu_slug' => 'my-jetpack',
1889                    'type'      => 'attention',
1890                )
1891            );
1892        }
1893
1894        $counted_elsewhere = self::get_alerts_counted_elsewhere();
1895        foreach ( array_keys( $red_bubble_alerts ) as $slug ) {
1896            if ( in_array( $slug, $counted_elsewhere, true ) ) {
1897                continue;
1898            }
1899            // The missing-connection slug and a connection error describe the same broken
1900            // connection; count it once (the error, above, is the more specific signal).
1901            if ( $has_connection_error && 'missing-connection' === $slug ) {
1902                continue;
1903            }
1904            Notification_Counts::register(
1905                'my-jetpack-' . $slug,
1906                array(
1907                    'menu_slug' => 'my-jetpack',
1908                    'type'      => 'attention',
1909                )
1910            );
1911        }
1912    }
1913
1914    /**
1915     * Whether the Connection Error Handler reports a displayable connection error
1916     * for the current viewer.
1917     *
1918     * Read live (not via the red-bubble transient): get_displayable_errors() is a
1919     * cached option read that the Error Handler already scopes and caches per viewer.
1920     * Guarded for the mid-plugin-update window, where a stale connection package
1921     * predating the method can be loaded.
1922     *
1923     * @return bool
1924     */
1925    private static function has_connection_error() {
1926        if ( ! class_exists( Connection_Error_Handler::class ) || ! method_exists( Connection_Error_Handler::class, 'get_displayable_errors' ) ) {
1927            return false;
1928        }
1929        return ! empty( Connection_Error_Handler::get_instance()->get_displayable_errors() );
1930    }
1931
1932    /**
1933     * Alerts left out of the My Jetpack badge because another menu item already counts them.
1934     *
1935     * The Protect page reports its own count, from the standalone plugin (class-jetpack-protect.php::admin_page_init())
1936     * or the Protect package's dashboard; without either, nobody else counts the threats, so they must stay.
1937     *
1938     * @return string[] Alert slugs.
1939     */
1940    private static function get_alerts_counted_elsewhere() {
1941        return Products\Protect::has_protect_page() ? array( 'protect_has_threats' ) : array();
1942    }
1943
1944    /**
1945     * Enqueue the notification bubble script.
1946     * Fetches fresh alert data via REST API without blocking page load.
1947     *
1948     * @return void
1949     */
1950    public static function enqueue_red_bubble_script() {
1951        Assets::register_script(
1952            'my-jetpack-notification-bubble',
1953            '../build/async-notification-bubble.js',
1954            __FILE__,
1955            array(
1956                'enqueue'   => true,
1957                'in_footer' => true,
1958            )
1959        );
1960        wp_localize_script(
1961            'my-jetpack-notification-bubble',
1962            'myJetpackNotificationBubble',
1963            array( 'excludedAlerts' => self::get_alerts_counted_elsewhere() )
1964        );
1965    }
1966
1967    /**
1968     * Get list of module names sorted by their recommendation score
1969     *
1970     * @return array|null
1971     */
1972    public static function get_recommended_modules() {
1973        $recommendations_evaluation = \Jetpack_Options::get_option( 'recommendations_evaluation', null );
1974
1975        if ( empty( $recommendations_evaluation ) || ! is_array( $recommendations_evaluation ) ) {
1976            return null;
1977        }
1978
1979        arsort( $recommendations_evaluation ); // Sort by scores in descending order
1980
1981        return array_keys( $recommendations_evaluation ); // Get only module names
1982    }
1983}