Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
50.57% covered (warning)
50.57%
222 / 439
15.00% covered (danger)
15.00%
3 / 20
CRAP
n/a
0 / 0
current_user_has_wpcom_account
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_add_dashboard_updates_menu
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
wpcom_display_dashboard_updates_page
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_can_link_to_calypso
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
wpcom_add_my_home_menu
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
wpcom_add_hosting_menu
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
wpcom_admin_menu_enqueue_styles
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_add_upgrades_menu
92.96% covered (success)
92.96%
66 / 71
0.00% covered (danger)
0.00%
0 / 1
3.00
wpcom_get_current_plan_name
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
7.19
wpcom_reorder_submenu
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
8.03
wpcom_add_jetpack_submenu
95.79% covered (success)
95.79%
91 / 95
0.00% covered (danger)
0.00%
0 / 1
18
wpcom_hide_customizer_submenu_on_block_theme
8.33% covered (danger)
8.33%
1 / 12
0.00% covered (danger)
0.00%
0 / 1
16.32
wpcom_maybe_enable_link_manager
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
4.01
wpcom_hide_submenu_page
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
wpcom_add_plugins_menu
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
182
wpcom_add_tools_menu
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
12
wpcom_redirect_retired_marketing_page
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
wpcom_display_export_erase_personal_data_page
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_display_site_health_page
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_add_settings_menu
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * WordPress.com admin menu
4 *
5 * Adds WordPress.com-specific stuff to WordPress admin menu.
6 *
7 * @package automattic/jetpack-mu-wpcom
8 */
9
10use Automattic\Jetpack\Connection\Manager as Connection_Manager;
11use Automattic\Jetpack\Modules;
12use Automattic\Jetpack\Newsletter\Settings as Newsletter_Settings;
13use Automattic\Jetpack\Podcast\Admin_Page as Podcast_Admin_Page;
14use Automattic\Jetpack\Redirect;
15
16require_once __DIR__ . '/../../common/wpcom-callout.php';
17require_once __DIR__ . '/../../common/launchpad-no-guidance.php';
18
19/**
20 * Checks if the current user has a WordPress.com account connected.
21 *
22 * @return bool
23 */
24function current_user_has_wpcom_account() {
25    $user_id = get_current_user_id();
26
27    if ( function_exists( '\A8C\Billingdaddy\Users\get_wpcom_user' ) ) {
28        // On Simple sites, use get_wpcom_user function to check if the user has a WordPress.com account.
29        $user        = \A8C\Billingdaddy\Users\get_wpcom_user( $user_id );
30        $has_account = isset( $user->ID );
31    } else {
32        // On Atomic sites, use the Connection Manager to check if the user has a WordPress.com account.
33        $connection_manager = new Connection_Manager();
34        $wpcom_user_data    = $connection_manager->get_connected_user_data( $user_id );
35        $has_account        = isset( $wpcom_user_data['ID'] );
36    }
37
38    return $has_account;
39}
40
41/**
42 * Adds the Dashboard > Updates menu on Simple sites
43 */
44function wpcom_add_dashboard_updates_menu() {
45    $is_simple_site = defined( 'IS_WPCOM' ) && IS_WPCOM;
46    if ( ! $is_simple_site || ( function_exists( 'wpcom_is_vip' ) && wpcom_is_vip() ) ) {
47        return;
48    }
49
50    add_submenu_page(
51        'index.php',
52        __( 'WordPress Updates', 'jetpack-mu-wpcom' ),
53        __( 'Updates', 'jetpack-mu-wpcom' ),
54        'manage_options',
55        'wpcom-dashboard-updates',
56        'wpcom_display_dashboard_updates_page'
57    );
58}
59add_action( 'admin_menu', 'wpcom_add_dashboard_updates_menu' );
60
61/**
62 * Displays a WordPress Updates page for Simple sites.
63 */
64function wpcom_display_dashboard_updates_page() {
65    ?>
66    <div class="wrap">
67        <h1><?php esc_html_e( 'WordPress Updates', 'jetpack-mu-wpcom' ); ?></h1>
68        <p><?php esc_html_e( "WordPress.com automatically keeps your site's plugins, themes, and WordPress version up to date.", 'jetpack-mu-wpcom' ); ?></p>
69        <h2><?php esc_html_e( 'WordPress', 'jetpack-mu-wpcom' ); ?></h2>
70        <p><?php esc_html_e( 'Your version of WordPress is up to date.', 'jetpack-mu-wpcom' ); ?></p>
71        <h2><?php esc_html_e( 'Plugins', 'jetpack-mu-wpcom' ); ?></h2>
72        <p><?php esc_html_e( 'Your plugins are all up to date.', 'jetpack-mu-wpcom' ); ?>
73        <h2><?php esc_html_e( 'Themes', 'jetpack-mu-wpcom' ); ?></h2>
74        <p><?php esc_html_e( 'Your themes are all up to date.', 'jetpack-mu-wpcom' ); ?>
75    </div>
76    <?php
77}
78
79/**
80 * Checks if menu items can link to Calypso.
81 *
82 * This way we can avoid a broken nav experience for super admins who are not members of the current site,
83 * since Calypso doesn't support this flow.
84 */
85function wpcom_can_link_to_calypso() {
86    return is_user_member_of_blog();
87}
88
89/**
90 * Adds a My Home menu.
91 */
92function wpcom_add_my_home_menu() {
93    if ( ! wpcom_can_link_to_calypso() ) {
94        return;
95    }
96
97    // My Home only goes with the legacy launchpad: AI Launchpad and no-guidance sites drop it for every user.
98    // Removing it here also drops it from the Calypso sidebar.
99    if ( get_option( 'wpcom_ai_launchpad_enabled' ) || wpcom_launchpad_is_no_guidance() ) {
100        return;
101    }
102
103    $domain = wp_parse_url( home_url(), PHP_URL_HOST );
104    // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
105    add_menu_page( __( 'My Home', 'jetpack-mu-wpcom' ), __( 'My Home', 'jetpack-mu-wpcom' ), 'read', 'https://wordpress.com/home/' . $domain, null, 'dashicons-admin-home', 2.01 ); // The 2.01 position is to ensure it's above the VIP menu on P2 sites.'
106}
107add_action( 'admin_menu', 'wpcom_add_my_home_menu' );
108
109/**
110 * Adds a Hosting menu.
111 */
112function wpcom_add_hosting_menu() {
113    $domain = wp_parse_url( home_url(), PHP_URL_HOST );
114
115    $menu_title = sprintf(
116        '%1$s<span class="inline-icon dashicons dashicons-external"></span>',
117        __( 'Hosting', 'jetpack-mu-wpcom' )
118    );
119
120    add_menu_page(
121        __( 'Hosting', 'jetpack-mu-wpcom' ),
122        $menu_title,
123        'manage_options',
124        esc_url( "https://wordpress.com/overview/$domain" ),
125        null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal
126        'dashicons-cloud',
127        2.98
128    );
129}
130add_action( 'admin_menu', 'wpcom_add_hosting_menu' );
131
132/**
133 * Enqueues admin menu styles.
134 */
135function wpcom_admin_menu_enqueue_styles() {
136    wp_enqueue_style(
137        'wpcom-admin-menu',
138        plugins_url( 'wpcom-admin-menu.css', __FILE__ ),
139        array(),
140        filemtime( __DIR__ . '/wpcom-admin-menu.css' )
141    );
142}
143add_action( 'admin_enqueue_scripts', 'wpcom_admin_menu_enqueue_styles' );
144
145/**
146 * Adds an Upgrades menu.
147 *
148 * This centralizes the Upgrades menu registration for all admin interfaces (Calypso and wp-admin).
149 * The masterbar classes defer to this function instead of registering their own Upgrades menu.
150 */
151function wpcom_add_upgrades_menu() {
152    // Don't show Upgrades on staging sites.
153    if ( get_option( 'wpcom_is_staging_site' ) ) {
154        return;
155    }
156
157    $domain      = wp_parse_url( home_url(), PHP_URL_HOST );
158    $parent_slug = 'paid-upgrades.php';
159
160    // Build the menu title, optionally with the plan label.
161    $plan = wpcom_get_current_plan_name();
162    if ( $plan ) {
163        // Add display:none as a default for cases when CSS is not loaded.
164        // Calypso and the masterbar CSS override this to show the plan label.
165        $menu_title = sprintf(
166            '%1$s<span class="inline-text" style="display:none">%2$s</span>',
167            __( 'Upgrades', 'jetpack-mu-wpcom' ),
168            esc_html( $plan )
169        );
170    } else {
171        $menu_title = __( 'Upgrades', 'jetpack-mu-wpcom' );
172    }
173
174    add_menu_page(
175        __( 'Upgrades', 'jetpack-mu-wpcom' ),
176        $menu_title,
177        'manage_options',
178        $parent_slug,
179        null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal
180        'dashicons-cart',
181        2.99
182    );
183
184    add_submenu_page(
185        $parent_slug,
186        __( 'Plans', 'jetpack-mu-wpcom' ),
187        __( 'Plans', 'jetpack-mu-wpcom' ),
188        'manage_options',
189        esc_url( "https://wordpress.com/plans/$domain" ),
190        null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal
191        1
192    );
193
194    add_submenu_page(
195        $parent_slug,
196        __( 'Add-ons', 'jetpack-mu-wpcom' ),
197        __( 'Add-ons', 'jetpack-mu-wpcom' ),
198        'manage_options',
199        esc_url( "https://wordpress.com/add-ons/$domain" ),
200        null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal
201        2
202    );
203
204    add_submenu_page(
205        $parent_slug,
206        __( 'Domains', 'jetpack-mu-wpcom' ),
207        __( 'Domains', 'jetpack-mu-wpcom' ),
208        'manage_options',
209        esc_url( "https://wordpress.com/domains/manage/$domain" ),
210        null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal
211        3
212    );
213
214    add_submenu_page(
215        $parent_slug,
216        __( 'Emails', 'jetpack-mu-wpcom' ),
217        __( 'Emails', 'jetpack-mu-wpcom' ),
218        'manage_options',
219        esc_url( "https://wordpress.com/email/$domain" ),
220        null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal
221        4
222    );
223
224    add_submenu_page(
225        $parent_slug,
226        __( 'Purchases', 'jetpack-mu-wpcom' ),
227        __( 'Purchases', 'jetpack-mu-wpcom' ),
228        'manage_options',
229        esc_url( "https://wordpress.com/purchases/subscriptions/$domain" ),
230        null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal
231        5
232    );
233
234    // By default, WordPress adds a submenu item for the parent menu item, which we don't want.
235    remove_submenu_page( $parent_slug, $parent_slug );
236
237    // Remove legacy Upgrades submenus registered elsewhere on WP.com.
238    remove_submenu_page( $parent_slug, 'premium-themes' );
239    remove_submenu_page( $parent_slug, 'domains' );
240    remove_submenu_page( $parent_slug, 'my-upgrades' );
241    remove_submenu_page( $parent_slug, 'billing-history' );
242}
243add_action( 'admin_menu', 'wpcom_add_upgrades_menu', 140 ); // After hookpress hook at 130, needed to ensure the legacy submenus are removed.
244
245/**
246 * Gets the current plan's short name.
247 *
248 * @return string|null The plan name, or null if unavailable.
249 */
250function wpcom_get_current_plan_name() {
251    // Simple sites: use WPCOM_Store_API.
252    if ( class_exists( 'WPCOM_Store_API' ) ) {
253        $current_plan = \WPCOM_Store_API::get_current_plan( get_current_blog_id() );
254        if ( ! empty( $current_plan['product_name_short'] ) ) {
255            return $current_plan['product_name_short'];
256        }
257    }
258
259    // Atomic sites: use Jetpack Current_Plan.
260    if ( class_exists( 'Automattic\Jetpack\Current_Plan' ) ) {
261        $products = \Automattic\Jetpack\Current_Plan::get();
262        if ( array_key_exists( 'product_name_short', $products ) ) {
263            return $products['product_name_short'];
264        }
265    }
266
267    return null;
268}
269
270/**
271 * Re-order the submenu items of the given menu slug according to a sorted array of submenu slugs.
272 *
273 * @param string $menu_slug The menu slug.
274 * @param array  $desired_order A list of the submenu slugs in the desired order.
275 */
276function wpcom_reorder_submenu( $menu_slug, $desired_order ) {
277    // Re-order menu.
278    global $submenu;
279    if ( ! isset( $submenu[ $menu_slug ] ) ) {
280        return;
281    }
282
283    $domain          = wp_parse_url( home_url(), PHP_URL_HOST );
284    $ordered_submenu = array();
285
286    // Re-add submenu items in the desired order. Dedupe because a slug in
287    // $desired_order can be a substring of another item's URL, which would
288    // otherwise match the same item twice.
289    foreach ( $desired_order as $submenu_slug ) {
290        foreach ( $submenu[ $menu_slug ] as $item ) {
291            $clean_url = str_replace( $domain, '', $item[2] );
292            if ( str_contains( $clean_url, $submenu_slug ) && ! in_array( $item, $ordered_submenu, true ) ) {
293                $ordered_submenu[] = $item;
294            }
295        }
296    }
297
298    // Add any remaining submenu items.
299    foreach ( $submenu[ $menu_slug ] as $item ) {
300        if ( ! in_array( $item, $ordered_submenu, true ) ) {
301            $ordered_submenu[] = $item;
302        }
303    }
304
305    // phpcs:ignore WordPress.WP.GlobalVariablesOverride
306    $submenu[ $menu_slug ] = $ordered_submenu;
307}
308
309/**
310 * Adds WordPress.com submenu items related to Jetpack under the Jetpack admin menu.
311 */
312function wpcom_add_jetpack_submenu() {
313    $is_simple_site = defined( 'IS_WPCOM' ) && IS_WPCOM;
314
315    $blog_id = Connection_Manager::get_site_id();
316    if ( is_wp_error( $blog_id ) ) {
317        return;
318    }
319
320    $domain = wp_parse_url( home_url(), PHP_URL_HOST );
321
322    // @codeCoverageIgnoreStart
323    // Hide certain Jetpack submenus for Atomic sites on Personal or Premium plans.
324    $is_personal_or_premium = false;
325    if ( class_exists( '\\Automattic\\Jetpack\\Current_Plan' ) ) {
326        $current_plan           = \Automattic\Jetpack\Current_Plan::get();
327        $plan_class             = $current_plan['class'] ?? '';
328        $is_personal_or_premium = in_array( $plan_class, array( 'personal', 'premium' ), true );
329    }
330
331    if ( ! $is_simple_site && $is_personal_or_premium ) {
332        // Jetpack > My Jetpack.
333        wpcom_hide_submenu_page( 'jetpack', 'my-jetpack' );
334
335        // Jetpack > Settings; WoA can pair this with a Jetpack serving either address.
336        wpcom_hide_submenu_page( 'jetpack', admin_url( 'admin.php?page=jetpack#/settings' ) );
337        wpcom_hide_submenu_page( 'jetpack', 'jetpack-settings' );
338
339        // Redirect My Jetpack page to Stats for Atomic sites on Personal or Premium plans.
340        add_action(
341            'admin_init',
342            function () {
343                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No action taken, just checking page.
344                if ( isset( $_GET['page'] ) && 'my-jetpack' === $_GET['page'] ) {
345                    /** This filter is documented in projects/packages/stats-admin/src/class-admin-bar.php */
346                    wp_safe_redirect( apply_filters( 'jetpack_stats_url', admin_url( 'admin.php?page=stats' ), array( 'view' => 'dashboard' ) ) );
347                    exit;
348                }
349            }
350        );
351
352        // Jetpack > Traffic (Calypso).
353        add_submenu_page(
354            'jetpack',
355            esc_attr__( 'Traffic', 'jetpack-mu-wpcom' ),
356            __( 'Traffic', 'jetpack-mu-wpcom' ),
357            'manage_options',
358            'https://wordpress.com/marketing/traffic/' . $domain,
359            null // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
360        );
361
362    }
363    // @codeCoverageIgnoreEnd
364
365    // Jetpack > Scan.
366    wpcom_hide_submenu_page( 'jetpack', esc_url( Redirect::get_url( 'cloud-scan-history-wp-menu' ) ) );
367    wpcom_hide_submenu_page( 'jetpack', esc_url( Redirect::get_url( 'calypso-scanner' ) ) );
368    add_submenu_page(
369        'jetpack',
370        /** "Scan" is a product name, do not translate. */
371        'Scan',
372        'Scan',
373        'manage_options',
374        'https://wordpress.com/scan/' . $domain,
375        null // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
376    );
377
378    // Jetpack > Backup. Calypso owns the nav, so hide the Jetpack plugin's own `jetpack-backup`
379    // entry; hidden rather than removed, so links into that page keep working.
380    wpcom_hide_submenu_page( 'jetpack', esc_url( Redirect::get_url( 'calypso-backups' ) ) );
381    wpcom_hide_submenu_page( 'jetpack', 'jetpack-backup' );
382    add_submenu_page(
383        'jetpack',
384        /** "Backup" is a product name, do not translate. */
385        'Backup',
386        'Backup',
387        'manage_options',
388        'https://wordpress.com/backup/' . $domain,
389        null // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
390    );
391
392    // Jetpack > Monetize.
393    add_submenu_page(
394        'jetpack',
395        esc_attr__( 'Monetize', 'jetpack-mu-wpcom' ),
396        __( 'Monetize', 'jetpack-mu-wpcom' ),
397        'manage_options',
398        'https://wordpress.com/earn/' . $domain,
399        null // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
400    );
401
402    // Jetpack > Subscribers. Always hide the auto-added Calypso redirect link.
403    wpcom_hide_submenu_page( 'jetpack', esc_url( Redirect::get_url( 'jetpack-menu-jetpack-manage-subscribers', array( 'site' => $blog_id ) ) ) );
404
405    // The unified Newsletter page owns the Subscribers tab on every site, so the legacy
406    // Calypso "Subscribers" submenu is retired. add_filter( 'rsm_jetpack_ui_modernization_newsletter',
407    // '__return_false' ) brings it back â€” the Calypso link only, not a wp-admin dashboard.
408    if ( ! apply_filters( 'rsm_jetpack_ui_modernization_newsletter', true ) ) {
409        add_submenu_page(
410            'jetpack',
411            __( 'Subscribers', 'jetpack-mu-wpcom' ),
412            __( 'Subscribers', 'jetpack-mu-wpcom' ),
413            'manage_options',
414            'https://wordpress.com/subscribers/' . $domain,
415            null // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
416        );
417    }
418
419    // Atomic loads Podcast through the Jetpack module, which the owner can switch
420    // off, and this builder runs either way. is_active() is always true on Simple,
421    // where the package loads unconditionally. The package ships with Jetpack, not this one.
422    if ( class_exists( Podcast_Admin_Page::class ) && ( new Modules() )->is_active( 'podcast' ) ) {
423        Podcast_Admin_Page::add_wp_admin_submenu();
424    }
425
426    if ( $is_simple_site ) {
427        // Jetpack > Newsletter.
428        // Register the in-admin Newsletter settings page (with its own render callback
429        // and admin hooks). This must be done here (at priority 999999) because the
430        // Jetpack menu is created by this function and doesn't exist at earlier priorities.
431        if ( class_exists( '\Automattic\Jetpack\Newsletter\Settings' ) ) {
432            // @phan-suppress-next-line PhanUndeclaredClassMethod -- class_exists guarded above; provided by sibling autoloader.
433            $newsletter_settings = new Newsletter_Settings();
434            // @phan-suppress-next-line PhanUndeclaredClassMethod -- class_exists guarded above; provided by sibling autoloader.
435            $newsletter_settings->add_wp_admin_submenu();
436        }
437
438        // Jetpack > VideoPress.
439        // Register the in-admin VideoPress dashboard page. Like Newsletter above, this
440        // must run here (priority 999999) because the Jetpack parent menu is created by
441        // this function and doesn't exist at earlier priorities. Gated on the VideoPress
442        // site feature so it only surfaces for VideoPress-enabled Simple sites during dev.
443        if (
444            class_exists( '\Automattic\Jetpack\VideoPress\Admin_UI' ) &&
445            function_exists( 'wpcom_site_has_feature' ) &&
446            class_exists( '\WPCOM_Features' ) &&
447            wpcom_site_has_feature( \WPCOM_Features::VIDEOPRESS )
448        ) {
449            \Automattic\Jetpack\VideoPress\Admin_UI::add_wp_admin_submenu();
450        }
451
452        // Jetpack > Traffic
453        add_submenu_page(
454            'jetpack',
455            __( 'Traffic', 'jetpack-mu-wpcom' ),
456            __( 'Traffic', 'jetpack-mu-wpcom' ),
457            'manage_options',
458            'https://wordpress.com/marketing/traffic/' . $domain,
459            null // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
460        );
461    }
462
463    // Jetpack > Activity Log.
464    // Atomic sites use the native Activity Log page that the `jetpack-activity-log`
465    // package registers at `admin.php?page=jetpack-activity-log`, whichever admin
466    // interface the site uses, and behave like a self-hosted site when that page is
467    // not available: the Calypso Activity Log screen is being retired. Simple sites
468    // still hide the native page and link to wordpress.com/activity-log.
469    if ( $is_simple_site ) {
470        wpcom_hide_submenu_page( 'jetpack', 'jetpack-activity-log' );
471        add_submenu_page(
472            'jetpack',
473            /** "Activity Log" is a product name, do not translate. */
474            'Activity Log',
475            'Activity Log',
476            'manage_options',
477            'https://wordpress.com/activity-log/' . $domain,
478            null // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
479        );
480    }
481
482    wpcom_reorder_submenu(
483        'jetpack',
484        array(
485            'my-jetpack',
486            'stats',
487            'advertising',
488            'boost',
489            'social',
490            'akismet-key-config',
491            'activity-log',
492            'scan',
493            'backup',
494            'forms',
495            'earn',
496            'search',
497            'subscribers',
498            'newsletter',
499            'podcast',
500            'traffic',
501            'jetpack#/settings',
502            'jetpack-settings',
503        )
504    );
505}
506add_action( 'admin_menu', 'wpcom_add_jetpack_submenu', 999999 );
507
508/*
509 * Prevents the Jetpack menu from being overridden on Simple sites.
510 *
511 * TODO: After deploying https://github.com/Automattic/jetpack/pull/39393, we can remove the `add_jetpack_submenu` function and this `remove_action` call.
512 */
513remove_action( 'admin_menu', 'add_jetpack_submenu', 999999 );
514
515/**
516 * Ensures customizer menu and admin bar items are not visible on a block theme.
517 */
518function wpcom_hide_customizer_submenu_on_block_theme() {
519    if ( wp_is_block_theme() && ! is_customize_preview() ) {
520        remove_action( 'customize_register', 'add_logotool_button', 20 );
521        remove_action( 'customize_register', 'footercredits_register', 99 );
522        remove_action( 'customize_register', 'wpcom_disable_customizer_site_icon', 20 );
523
524        if ( class_exists( '\Jetpack_Fonts' ) ) {
525            $jetpack_fonts_instance = \Jetpack_Fonts::get_instance();
526            remove_action( 'customize_register', array( $jetpack_fonts_instance, 'register_controls' ) );
527            remove_action( 'customize_register', array( $jetpack_fonts_instance, 'maybe_prepopulate_option' ), 0 );
528        }
529
530        remove_action( 'customize_register', array( 'Jetpack_Fonts_Typekit', 'maybe_override_for_advanced_mode' ), 20 );
531
532        remove_action( 'customize_register', 'Automattic\Jetpack\Masterbar\register_css_nudge_control' );
533
534        remove_action( 'customize_register', array( 'Jetpack_Custom_CSS_Customizer', 'customize_register' ) );
535
536        remove_action( 'customize_register', array( 'Jetpack_Custom_CSS_Enhancements', 'customize_register' ) );
537    }
538}
539
540$customizer_removal_hook = defined( 'REST_API_REQUEST' ) && REST_API_REQUEST ? 'rest_pre_dispatch' : 'init';
541add_action( $customizer_removal_hook, 'wpcom_hide_customizer_submenu_on_block_theme' );
542
543/**
544 * Links were removed in 3.5 core, but we've kept them active on dotcom.
545 *
546 * This function will check to see if Links should be enabled based on the number of links in the database
547 * and then set an option to minimize repeat queries later. The Links menu is visible when the Link Manager is enabled.
548 *
549 * @return void
550 */
551function wpcom_maybe_enable_link_manager() {
552    if ( get_option( 'link_manager_check' ) ) {
553        return;
554    }
555
556    // The max ID number of the auto-generated links.
557    // See /wp-content/mu-plugins/wpcom-wp-install-defaults.php in WP.com.
558    $max_default_id = 10;
559
560    // We are only checking the latest entry link_id so are limiting the query to 1.
561    $link_manager_links = get_bookmarks(
562        array(
563            'orderby'        => 'link_id',
564            'order'          => 'DESC',
565            'limit'          => 1,
566            'hide_invisible' => 0,
567        )
568    );
569
570    $has_links = is_countable( $link_manager_links ) && count( $link_manager_links ) > 0 && $link_manager_links[0]->link_id > $max_default_id;
571
572    update_option( 'link_manager_enabled', intval( $has_links ) );
573    update_option( 'link_manager_check', time() );
574}
575add_action( 'init', 'wpcom_maybe_enable_link_manager' );
576
577/**
578 * Hides a submenu item.
579 *
580 * Useful in cases where we cannot remove a submenu item because there is external logic
581 * that depends on the route registered by that submenu.
582 *
583 * @param string $menu_slug The slug of the parent menu.
584 * @param string $submenu_slug The slug of the submenu that should be hidden.
585 */
586function wpcom_hide_submenu_page( string $menu_slug, string $submenu_slug ) {
587    global $submenu;
588
589    if ( ! isset( $submenu[ $menu_slug ] ) ) {
590        return;
591    }
592
593    foreach ( $submenu[ $menu_slug ] as $i => $item ) {
594        if ( $submenu_slug !== $item[2] ) {
595            continue;
596        }
597
598        $css_hide_class = 'hide-if-js';
599        $css_classes    = empty( $item[4] ) ? $css_hide_class : $item[4] . ' ' . $css_hide_class;
600
601        // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
602        $submenu[ $menu_slug ][ $i ][4] = $css_classes;
603        return;
604    }
605}
606
607/**
608 * Handles the Plugins menu.
609 */
610function wpcom_add_plugins_menu() {
611    global $menu;
612    $is_simple_site          = defined( 'IS_WPCOM' ) && IS_WPCOM;
613    $is_atomic_site          = ! $is_simple_site;
614    $uses_wp_admin_interface = get_option( 'wpcom_admin_interface' ) === 'wp-admin';
615
616    if ( $is_simple_site ) {
617        $has_plugins_menu = false;
618        foreach ( $menu as &$menu_item ) {
619            if ( 'plugins.php' === $menu_item[2] ) {
620                $has_plugins_menu = true;
621                break;
622            }
623        }
624
625        if ( ! $has_plugins_menu ) {
626            // TODO: Remove `remove_menu_page('plugins.php');` from `/wp-content/admin-plugins/wpcom-misc.php`.
627            add_menu_page(
628                __( 'Plugins', 'jetpack-mu-wpcom' ),
629                __( 'Plugins', 'jetpack-mu-wpcom' ),
630                'manage_options', // Roughly means "is a site admin"
631                'plugins.php',
632                null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal
633                'dashicons-admin-plugins',
634                65
635            );
636        }
637
638        if ( function_exists( 'wpcom_plugins_display_marketplace' ) ) {
639            add_submenu_page(
640                'plugins.php',
641                __( 'Add New Plugin', 'jetpack-mu-wpcom' ),
642                __( 'Add New Plugin', 'jetpack-mu-wpcom' ),
643                'manage_options', // Roughly means "is a site admin"
644                'wpcom-install-plugin',
645                'wpcom_plugins_display_marketplace'
646            );
647
648            if ( ! $uses_wp_admin_interface ) {
649                wpcom_hide_submenu_page( 'plugins.php', 'wpcom-install-plugin' );
650            }
651        }
652    }
653
654    $domain = wp_parse_url( home_url(), PHP_URL_HOST );
655    if ( $uses_wp_admin_interface ) {
656        add_submenu_page(
657            'plugins.php',
658            /* translators: Name of the Plugins submenu that links to the Plugins Marketplace */
659                __( 'Marketplace', 'jetpack-mu-wpcom' ),
660            /* translators: Name of the Plugins submenu that links to the Plugins Marketplace */
661                __( 'Marketplace', 'jetpack-mu-wpcom' ),
662            'manage_options', // Roughly means "is a site admin"
663            'https://wordpress.com/plugins/' . $domain,
664            null // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal
665        );
666    }
667
668    if ( $is_atomic_site ) {
669        if (
670            ! get_option( 'wpcom_is_staging_site' ) &&
671            function_exists( 'wpcom_site_has_feature' ) &&
672            wpcom_site_has_feature( \WPCOM_Features::SCHEDULED_UPDATES )
673        ) {
674            add_submenu_page(
675                'plugins.php',
676                esc_attr__( 'Scheduled Updates', 'jetpack-mu-wpcom' ),
677                __( 'Scheduled Updates', 'jetpack-mu-wpcom' ),
678                'update_plugins',
679                esc_url( "https://wordpress.com/plugins/scheduled-updates/$domain" ),
680                null // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal
681            );
682        }
683    }
684}
685add_action( 'admin_menu', 'wpcom_add_plugins_menu' );
686
687/**
688 * Adds some Tools menus that are missing on Simple sites.
689 */
690function wpcom_add_tools_menu() {
691    $is_simple_site = defined( 'IS_WPCOM' ) && IS_WPCOM;
692    if ( $is_simple_site ) {
693        add_submenu_page(
694            'tools.php',
695            __( 'Export Personal Data', 'jetpack-mu-wpcom' ),
696            __( 'Export Personal Data', 'jetpack-mu-wpcom' ),
697            'manage_options',
698            'wpcom-export-personal-data',
699            'wpcom_display_export_erase_personal_data_page'
700        );
701
702        add_submenu_page(
703            'tools.php',
704            __( 'Erase Personal Data', 'jetpack-mu-wpcom' ),
705            __( 'Erase Personal Data', 'jetpack-mu-wpcom' ),
706            'manage_options',
707            'wpcom-erase-personal-data',
708            'wpcom_display_export_erase_personal_data_page'
709        );
710
711        add_submenu_page(
712            'tools.php',
713            __( 'Site Health', 'jetpack-mu-wpcom' ),
714            __( 'Site Health', 'jetpack-mu-wpcom' ),
715            'manage_options',
716            'wpcom-site-health',
717            'wpcom_display_site_health_page'
718        );
719    }
720
721    wpcom_reorder_submenu(
722        'tools.php',
723        array(
724            'tools.php',
725            'advertising-moved',
726            'monetize',
727            'import',
728            'export.php',
729            'export-media-files',
730            'site-health',
731            'export-personal-data',
732            'erase-personal-data',
733            'theme-editor',
734            'plugin-editor',
735        )
736    );
737}
738add_action( 'admin_menu', 'wpcom_add_tools_menu', 999999 );
739
740/**
741 * Sends the retired Tools > Marketing URL to the dashboard.
742 *
743 * The page was removed in DOTCOM-18531. Nothing links to it any more, but the
744 * Calypso sidebar pointed at this exact URL until the removal shipped, so it is
745 * still in plenty of browser histories and bookmarks. Left alone, WordPress
746 * answers an unregistered page slug with a bare "Cannot load
747 * wpcom-marketing-tools.", which reads like a broken install rather than a page
748 * that went away. The dashboard mirrors where the Calypso URL now lands.
749 *
750 * This runs on admin_init, which fires before the point in wp-admin/admin.php
751 * where core gives up on the slug.
752 *
753 * Tombstone: delete this function and its hook by 2027-09-10. A year is long
754 * enough for the stale bookmarks to age out, and after that the redirect is
755 * just a puzzle for whoever reads this next.
756 */
757function wpcom_redirect_retired_marketing_page() {
758    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading a menu slug from a GET request, no state changes.
759    if ( ! isset( $_GET['page'] ) ) {
760        return;
761    }
762
763    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- As above.
764    if ( 'wpcom-marketing-tools' !== sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) {
765        return;
766    }
767
768    wp_safe_redirect( admin_url() );
769    exit; // @codeCoverageIgnore -- the tests unwind from the wp_redirect filter before this line.
770}
771add_action( 'admin_init', 'wpcom_redirect_retired_marketing_page' ); // @codeCoverageIgnore
772
773/**
774 * Displays an Export/Erase Personal Date page for Simple sites.
775 */
776function wpcom_display_export_erase_personal_data_page() {
777    if ( str_contains( get_current_screen()->id, 'export-personal-data' ) ) {
778        $page_title = __( 'Export Personal Data', 'jetpack-mu-wpcom' );
779    } else {
780        $page_title = __( 'Erase Personal Data', 'jetpack-mu-wpcom' );
781    }
782
783    wpcom_display_callout(
784        'dashicons-id-alt',
785        $page_title,
786        array(
787            __( 'WordPress.com gives you the tools to manage personal data from your dashboard, like viewing or deleting comments tied to a visitor.', 'jetpack-mu-wpcom' ),
788            __( 'To support privacy requests, make sure people can reach you easily, whether through a contact form or an email in your Privacy Policy.', 'jetpack-mu-wpcom' ),
789        ),
790        localized_wpcom_url( 'https://wordpress.com/support/your-site-and-the-gdpr/' ),
791        __( 'Learn more', 'jetpack-mu-wpcom' ),
792        plugins_url( 'images/performance.svg', __FILE__ )
793    );
794}
795
796/**
797 * Displays a callout on Simple Sites for Tools > Site Health menu.
798 *
799 * @return void
800 */
801function wpcom_display_site_health_page() {
802    wpcom_display_callout(
803        'dashicons-admin-site-alt3',
804        __( 'Your site\'s in good hands', 'jetpack-mu-wpcom' ),
805        array(
806            __( 'No need to stress over performance or security checks, WordPress.com handles that for you behind the scenes.', 'jetpack-mu-wpcom' ),
807            __( 'That way, your site stays fast, safe, and reliable, without any extra effort from you.', 'jetpack-mu-wpcom' ),
808        ),
809        localized_wpcom_url( 'https://wordpress.com/support/choose-a-host/#frequently-asked-questions-about-managed-hosting-with-word-press-com' ),
810        __( 'Learn more', 'jetpack-mu-wpcom' ),
811        plugins_url( 'images/cloud.svg', __FILE__ )
812    );
813}
814
815/**
816 * Adjust the Settings submenus so they are sorted consistently.
817 */
818function wpcom_add_settings_menu() {
819    wpcom_reorder_submenu(
820        'options-general.php',
821        array(
822            'general',
823            'writing',
824            'reading',
825            'discussion',
826            'media',
827            'permalink',
828            'privacy',
829            'sharing',
830            'optimize',
831            'crowdsignal',
832            'rating',
833            'newsletter',
834        )
835    );
836}
837add_action( 'admin_menu', 'wpcom_add_settings_menu', 999999 );
838
839if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
840    require_once __DIR__ . '/p2-admin-menu.php';
841}