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