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