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