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