Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
28.06% covered (danger)
28.06%
39 / 139
17.65% covered (danger)
17.65%
3 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPcom_Admin_Menu
28.26% covered (danger)
28.26%
39 / 138
17.65% covered (danger)
17.65%
3 / 17
898.65
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 reregister_menu_items
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 get_preferred_view
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
 get_current_user_blog_count
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 set_browse_sites_link_class
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 add_new_site_link
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 get_upsell_nudge
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
6
 get_current_plan
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 add_upgrades_menu
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
5.68
 add_appearance_menu
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 add_users_menu
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 remove_gutenberg_menu
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 should_link_to_wp_admin
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 ajax_sidebar_state
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 wp_ajax_jitm_dismiss
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 sync_sidebar_collapsed_state
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 remove_submenus
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * WP.com Admin Menu file.
4 *
5 * @package automattic/jetpack-masterbar
6 */
7
8namespace Automattic\Jetpack\Masterbar;
9
10use Jetpack_Custom_CSS;
11use JITM;
12
13require_once __DIR__ . '/class-admin-menu.php';
14
15/**
16 * Class WPcom_Admin_Menu.
17 */
18class WPcom_Admin_Menu extends Admin_Menu {
19    /**
20     * Holds the current plan, set by get_current_plan().
21     *
22     * @var array
23     */
24    private $current_plan = array();
25
26    /**
27     * WPcom_Admin_Menu constructor.
28     */
29    protected function __construct() {
30        parent::__construct();
31
32        add_action( 'wp_ajax_sidebar_state', array( $this, 'ajax_sidebar_state' ) );
33        add_action( 'wp_ajax_jitm_dismiss', array( $this, 'wp_ajax_jitm_dismiss' ) );
34        add_action( 'wp_ajax_upsell_nudge_jitm', array( $this, 'wp_ajax_upsell_nudge_jitm' ) );
35        add_action( 'admin_enqueue_scripts', array( $this, 'wpcom_upsell_nudge_jitm_fix' ) );
36        add_action( 'admin_init', array( $this, 'sync_sidebar_collapsed_state' ) );
37        add_action( 'admin_menu', array( $this, 'remove_submenus' ), 140 ); // After hookpress hook at 130.
38    }
39
40    /**
41     * Create the desired menu output.
42     */
43    public function reregister_menu_items() {
44        parent::reregister_menu_items();
45
46        $this->remove_gutenberg_menu();
47
48        // Not needed outside of wp-admin.
49        if ( ! $this->is_api_request ) {
50            $this->add_new_site_link();
51        }
52
53        ksort( $GLOBALS['menu'] );
54    }
55
56    /**
57     * Get the preferred view for the given screen.
58     *
59     * @param string $screen Screen identifier.
60     * @param bool   $fallback_global_preference (Optional) Whether the global preference for all screens should be used
61     *                                           as fallback if there is no specific preference for the given screen.
62     *                                           Default: true.
63     * @return string
64     */
65    public function get_preferred_view( $screen, $fallback_global_preference = true ) {
66        // When no preferred view has been set for Themes, keep the previous behavior that forced the default view
67        // regardless of the global preference.
68        if ( $fallback_global_preference && 'themes.php' === $screen ) {
69            $preferred_view = parent::get_preferred_view( $screen, false );
70            if ( self::UNKNOWN_VIEW === $preferred_view ) {
71                return self::DEFAULT_VIEW;
72            }
73            return $preferred_view;
74        }
75
76        // Plugins on Simple sites are always managed on Calypso.
77        if ( 'plugins.php' === $screen ) {
78            return self::DEFAULT_VIEW;
79        }
80
81        return parent::get_preferred_view( $screen, $fallback_global_preference );
82    }
83
84    /**
85     * Retrieve the number of blogs that the current user has.
86     *
87     * @return int
88     */
89    public function get_current_user_blog_count() {
90        if ( function_exists( '\get_blog_count_for_user' ) ) {
91            return \get_blog_count_for_user( get_current_user_id() );
92        }
93
94        $blogs = get_blogs_of_user( get_current_user_id() );
95        return is_countable( $blogs ) ? count( $blogs ) : 0;
96    }
97
98    /**
99     * Adds a custom element class for Site Switcher menu item.
100     *
101     * @param array $menu Associative array of administration menu items.
102     * @return array
103     */
104    public function set_browse_sites_link_class( array $menu ) {
105        foreach ( $menu as $key => $menu_item ) {
106            if ( 'site-switcher' !== $menu_item[3] ) {
107                continue;
108            }
109
110            $menu[ $key ][4] = add_cssclass( 'site-switcher', $menu_item[4] );
111            break;
112        }
113
114        return $menu;
115    }
116
117    /**
118     * Adds a link to the menu to create a new site.
119     */
120    public function add_new_site_link() {
121        if ( $this->get_current_user_blog_count() > 1 ) {
122            return;
123        }
124
125        $this->add_admin_menu_separator();
126        // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
127        add_menu_page( __( 'Add New Site', 'jetpack-masterbar' ), __( 'Add New Site', 'jetpack-masterbar' ), 'read', 'https://wordpress.com/start?ref=calypso-sidebar', null, 'dashicons-plus-alt' );
128    }
129
130    /**
131     * Returns the first available upsell nudge.
132     *
133     * @return array
134     */
135    public function get_upsell_nudge() {
136        require_lib( 'jetpack-jitm/jitm-engine' );
137        $jitm_engine = new JITM\Engine();
138
139        $message_path = 'calypso:sites:sidebar_notice';
140        $current_user = wp_get_current_user();
141        $user_id      = $current_user->ID;
142        $user_roles   = implode( ',', $current_user->roles );
143        $query_string = array(
144            'message_path' => $message_path,
145        );
146
147        // Get the top message only.
148        $message = $jitm_engine->get_top_messages( $message_path, $user_id, $user_roles, $query_string );
149
150        if ( isset( $message[0] ) ) {
151            $message = $message[0];
152            return array(
153                'content'                      => $message->content['message'],
154                'cta'                          => $message->CTA['message'], // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
155                'link'                         => $message->CTA['link'], // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
156                'tracks_impression_event_name' => $message->tracks['display']['name'] ?? null,
157                'tracks_impression_cta_name'   => $message->tracks['display']['props']['cta_name'] ?? null,
158                'tracks_click_event_name'      => $message->tracks['click']['name'] ?? null,
159                'tracks_click_cta_name'        => $message->tracks['click']['props']['cta_name'] ?? null,
160                'dismissible'                  => $message->is_dismissible,
161                'feature_class'                => $message->feature_class,
162                'id'                           => $message->id,
163            );
164        }
165    }
166
167    /**
168     * Gets the current plan and stores it in $this->current_plan so the database is only called once per request.
169     *
170     * @return array
171     */
172    private function get_current_plan() {
173        if ( empty( $this->current_plan ) && class_exists( 'WPCOM_Store_API' ) ) {
174            $this->current_plan = \WPCOM_Store_API::get_current_plan( get_current_blog_id() );
175        }
176        return $this->current_plan;
177    }
178
179    /**
180     * Adds Upgrades menu.
181     *
182     * @param string $plan The current WPCOM plan of the blog.
183     */
184    public function add_upgrades_menu( $plan = null ) {
185        $current_plan = $this->get_current_plan();
186        if ( ! empty( $current_plan['product_name_short'] ) ) {
187            $plan = $current_plan['product_name_short'];
188        }
189
190        parent::add_upgrades_menu( $plan );
191
192        $last_upgrade_submenu_position = $this->get_submenu_item_count( 'paid-upgrades.php' );
193
194        // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
195        add_submenu_page( 'paid-upgrades.php', __( 'Domains', 'jetpack-masterbar' ), __( 'Domains', 'jetpack-masterbar' ), 'manage_options', 'https://wordpress.com/domains/manage/' . $this->domain, null, $last_upgrade_submenu_position - 1 );
196
197        /** This filter is already documented in modules/masterbar/admin-menu/class-atomic-admin-menu.php */
198        if ( apply_filters( 'jetpack_show_wpcom_upgrades_email_menu', false ) ) {
199            // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
200            add_submenu_page( 'paid-upgrades.php', __( 'Emails', 'jetpack-masterbar' ), __( 'Emails', 'jetpack-masterbar' ), 'manage_options', 'https://wordpress.com/email/' . $this->domain, null, $last_upgrade_submenu_position );
201        }
202
203        if ( defined( 'WPCOM_ENABLE_ADD_ONS_MENU_ITEM' ) && WPCOM_ENABLE_ADD_ONS_MENU_ITEM ) {
204            // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
205            add_submenu_page( 'paid-upgrades.php', __( 'Add-Ons', 'jetpack-masterbar' ), __( 'Add-Ons', 'jetpack-masterbar' ), 'manage_options', 'https://wordpress.com/add-ons/' . $this->domain, null, 1 );
206        }
207    }
208
209    /**
210     * Adds Appearance menu.
211     *
212     * @return string The Customizer URL.
213     */
214    public function add_appearance_menu() {
215        $customize_url = parent::add_appearance_menu();
216
217        $this->hide_submenu_page( 'themes.php', 'theme-editor.php' );
218
219        $user_can_customize = current_user_can( 'customize' );
220
221        if ( wp_is_block_theme() ) {
222            add_filter( 'safecss_is_freetrial', '__return_false', PHP_INT_MAX );
223            if ( class_exists( 'Jetpack_Custom_CSS' ) && empty( Jetpack_Custom_CSS::get_css() ) ) {
224                $user_can_customize = false;
225            }
226            remove_filter( 'safecss_is_freetrial', '__return_false', PHP_INT_MAX );
227        }
228
229        if ( $user_can_customize ) {
230            $customize_custom_css_url = add_query_arg( array( 'autofocus' => array( 'section' => 'jetpack_custom_css' ) ), $customize_url );
231            // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
232            add_submenu_page( 'themes.php', esc_attr__( 'Additional CSS', 'jetpack-masterbar' ), __( 'Additional CSS', 'jetpack-masterbar' ), 'customize', esc_url( $customize_custom_css_url ), null, 20 );
233        }
234
235        return $customize_url;
236    }
237
238    /**
239     * Adds Users menu.
240     */
241    public function add_users_menu() {
242        $submenus_to_update = array(
243            'grofiles-editor'        => 'https://wordpress.com/me',
244            'grofiles-user-settings' => 'https://wordpress.com/me/account',
245        );
246
247        if ( self::DEFAULT_VIEW === $this->get_preferred_view( 'users.php' ) ) {
248            $submenus_to_update['users.php'] = 'https://wordpress.com/people/team/' . $this->domain;
249        }
250
251        $slug = current_user_can( 'list_users' ) ? 'users.php' : 'profile.php';
252        $this->update_submenus( $slug, $submenus_to_update );
253        // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
254        add_submenu_page( 'users.php', esc_attr__( 'Add New User', 'jetpack-masterbar' ), __( 'Add New User', 'jetpack-masterbar' ), 'promote_users', 'https://wordpress.com/people/new/' . $this->domain, null, 1 );
255    }
256
257    /**
258     * Also remove the Gutenberg plugin menu.
259     */
260    public function remove_gutenberg_menu() {
261        // Always remove the Gutenberg menu.
262        remove_menu_page( 'gutenberg' );
263    }
264
265    /**
266     * Whether to use wp-admin pages rather than Calypso.
267     *
268     * @return bool
269     */
270    public function should_link_to_wp_admin() {
271        $result = false; // Calypso.
272
273        $user_attribute = get_user_attribute( get_current_user_id(), 'calypso_preferences' );
274        if ( ! empty( $user_attribute['linkDestination'] ) ) {
275            $result = $user_attribute['linkDestination'];
276        }
277
278        return $result;
279    }
280
281    /**
282     * Saves the sidebar state ( expanded / collapsed ) via an ajax request.
283     *
284     * @return never
285     */
286    public function ajax_sidebar_state() {
287        $expanded    = isset( $_REQUEST['expanded'] ) ? filter_var( wp_unslash( $_REQUEST['expanded'] ), FILTER_VALIDATE_BOOLEAN ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
288        $user_id     = get_current_user_id();
289        $preferences = get_user_attribute( $user_id, 'calypso_preferences' );
290        if ( empty( $preferences ) ) {
291            $preferences = array();
292        }
293
294        $value = array_merge( (array) $preferences, array( 'sidebarCollapsed' => ! $expanded ) );
295        $value = array_filter(
296            $value,
297            function ( $preference ) {
298                return null !== $preference;
299            }
300        );
301
302        update_user_attribute( $user_id, 'calypso_preferences', $value );
303
304        die( 0 );
305    }
306
307    /**
308     * Handle ajax requests to dismiss a just-in-time-message
309     */
310    public function wp_ajax_jitm_dismiss() {
311        check_ajax_referer( 'jitm_dismiss' );
312        require_lib( 'jetpack-jitm/jitm-engine' );
313        if ( isset( $_REQUEST['id'] ) && isset( $_REQUEST['feature_class'] ) ) {
314            JITM\Engine::dismiss( sanitize_text_field( wp_unslash( $_REQUEST['id'] ) ), sanitize_text_field( wp_unslash( $_REQUEST['feature_class'] ) ) );
315        }
316        wp_die();
317    }
318
319    /**
320     * Syncs the sidebar collapsed state from Calypso Preferences.
321     */
322    public function sync_sidebar_collapsed_state() {
323        $calypso_preferences = get_user_attribute( get_current_user_id(), 'calypso_preferences' );
324
325        $sidebar_collapsed = $calypso_preferences['sidebarCollapsed'] ?? false;
326
327        // Read the current stored setting and convert it to boolean in order to be able to compare the values later.
328        $current_sidebar_collapsed_setting = 'f' === get_user_setting( 'mfold' );
329
330        // Only set the setting if the value differs, as `set_user_setting` always updates at least the timestamp
331        // which leads to unnecessary user meta cache purging on all wp-admin screen requests.
332        if ( $current_sidebar_collapsed_setting !== $sidebar_collapsed ) {
333            set_user_setting( 'mfold', $sidebar_collapsed ? 'f' : 'o' );
334        }
335    }
336
337    /**
338     * Removes unwanted submenu items.
339     *
340     * These submenus are added across wp-content and should be removed together with these function calls.
341     */
342    public function remove_submenus() {
343        global $_registered_pages;
344
345        remove_submenu_page( 'index.php', 'akismet-stats' );
346        remove_submenu_page( 'index.php', 'my-comments' );
347        remove_submenu_page( 'index.php', 'stats' );
348        remove_submenu_page( 'index.php', 'subscriptions' );
349
350        /* @see https://github.com/Automattic/wp-calypso/issues/49210 */
351        remove_submenu_page( 'index.php', 'my-blogs' );
352        $_registered_pages['admin_page_my-blogs'] = true; // phpcs:ignore
353
354        remove_submenu_page( 'paid-upgrades.php', 'premium-themes' );
355        remove_submenu_page( 'paid-upgrades.php', 'domains' );
356        remove_submenu_page( 'paid-upgrades.php', 'my-upgrades' );
357        remove_submenu_page( 'paid-upgrades.php', 'billing-history' );
358
359        remove_submenu_page( 'themes.php', 'customize.php?autofocus[panel]=amp_panel&return=' . rawurlencode( admin_url() ) );
360
361        remove_submenu_page( 'users.php', 'wpcom-invite-users' ); // Wpcom_Invite_Users::action_admin_menu.
362
363        remove_submenu_page( 'options-general.php', 'adcontrol' );
364
365        // Remove menu item but continue allowing access.
366        foreach ( array( 'openidserver', 'webhooks' ) as $page_slug ) {
367            remove_submenu_page( 'options-general.php', $page_slug );
368            $_registered_pages[ 'admin_page_' . $page_slug ] = true; // phpcs:ignore
369        }
370    }
371}