Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.16% covered (warning)
65.16%
159 / 244
55.56% covered (warning)
55.56%
15 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
Base_Admin_Menu
65.16% covered (warning)
65.16%
159 / 244
55.56% covered (warning)
55.56%
15 / 27
494.77
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 get_instance
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 update_menu
71.43% covered (warning)
71.43%
20 / 28
0.00% covered (danger)
0.00%
0 / 1
18.57
 update_submenus
96.15% covered (success)
96.15%
25 / 26
0.00% covered (danger)
0.00%
0 / 1
7
 add_admin_menu_separator
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 enqueue_scripts
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 configure_colors_for_rtl_stylesheets
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hide_submenu_page
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 hide_submenu_element
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 has_visible_items
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 get_submenu_item_count
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
 set_menu_item
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 is_rtl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 override_svg_icons
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
110
 hide_parent_of_hidden_submenus
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
8
 sort_hidden_submenus
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 is_item_visible
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 add_dashboard_switcher
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 dashboard_switcher_scripts
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 set_preferred_view
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 get_preferred_views
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 get_preferred_view
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 get_current_screen
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
6.32
 handle_preferred_view
62.50% covered (warning)
62.50%
10 / 16
0.00% covered (danger)
0.00%
0 / 1
7.90
 admin_body_class
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 should_link_to_wp_admin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 use_wp_admin_interface
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 reregister_menu_items
n/a
0 / 0
n/a
0 / 0
0
1<?php
2/**
3 * Base Admin Menu file.
4 *
5 * @package automattic/jetpack-masterbar
6 */
7
8namespace Automattic\Jetpack\Masterbar;
9
10use Automattic\Jetpack\Assets;
11use Automattic\Jetpack\Status;
12
13/**
14 * Class Base_Admin_Menu
15 */
16abstract class Base_Admin_Menu {
17    /**
18     * Holds class instances.
19     *
20     * @var array
21     */
22    protected static $instances;
23
24    /**
25     * Whether the current request is a REST API request.
26     *
27     * @var bool
28     */
29    protected $is_api_request = false;
30
31    /**
32     * Domain of the current site.
33     *
34     * @var string
35     */
36    protected $domain;
37
38    /**
39     * The CSS classes used to hide the submenu items in navigation.
40     *
41     * @var string
42     */
43    const HIDE_CSS_CLASS = 'hide-if-js';
44
45    /**
46     * Identifier denoting that the default WordPress.com view should be used for a certain screen.
47     *
48     * @var string
49     */
50    const DEFAULT_VIEW = 'default';
51
52    /**
53     * Identifier denoting that the classic WP Admin view should be used for a certain screen.
54     *
55     * @var string
56     */
57    const CLASSIC_VIEW = 'classic';
58
59    /**
60     * Identifier denoting no preferred view has been set for a certain screen.
61     *
62     * @var string
63     */
64    const UNKNOWN_VIEW = 'unknown';
65
66    /**
67     * Base_Admin_Menu constructor.
68     */
69    protected function __construct() {
70        $this->is_api_request = defined( 'REST_REQUEST' ) && REST_REQUEST || isset( $_SERVER['REQUEST_URI'] ) && str_starts_with( filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ) ), '/?rest_route=%2Fwpcom%2Fv2%2Fadmin-menu' );
71        $this->domain         = ( new Status() )->get_site_suffix();
72
73        add_action( 'admin_menu', array( $this, 'reregister_menu_items' ), 99998 );
74        add_action( 'admin_menu', array( $this, 'hide_parent_of_hidden_submenus' ), 99999 );
75
76        if ( ! $this->is_api_request ) {
77            add_filter( 'admin_menu', array( $this, 'override_svg_icons' ), 99999 );
78            add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ), 11 );
79            add_action( 'in_admin_header', array( $this, 'add_dashboard_switcher' ) );
80            add_action( 'admin_footer', array( $this, 'dashboard_switcher_scripts' ) );
81            add_action( 'admin_menu', array( $this, 'handle_preferred_view' ), 99997 );
82            add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
83        }
84    }
85
86    /**
87     * Returns class instance.
88     *
89     * @return static
90     */
91    public static function get_instance() {
92        $class = static::class;
93
94        if ( empty( static::$instances[ $class ] ) ) {
95            // @phan-suppress-next-line PhanTypeInstantiateAbstract -- If someone calls `Admin_Menu_Base::get_instance()` they deserve what they get.
96            static::$instances[ $class ] = new $class();
97        }
98
99        return static::$instances[ $class ];
100    }
101
102    /**
103     * Updates the menu data of the given menu slug.
104     *
105     * @param string  $slug Slug of the menu to update.
106     * @param ?string $url New menu URL. Defaults to null.
107     * @param ?string $title New menu title. Defaults to null.
108     * @param ?string $cap New menu capability. Defaults to null.
109     * @param ?string $icon New menu icon. Defaults to null.
110     * @param ?int    $position New menu position. Defaults to null.
111     * @return bool Whether the menu has been updated.
112     */
113    public function update_menu( $slug, $url = null, $title = null, $cap = null, $icon = null, $position = null ) {
114        global $menu, $submenu;
115
116        $menu_item     = null;
117        $menu_position = 0;
118
119        foreach ( $menu as $i => $item ) {
120            if ( $slug === $item[2] ) {
121                $menu_item     = $item;
122                $menu_position = $i;
123                break;
124            }
125        }
126
127        if ( ! $menu_item ) {
128            return false;
129        }
130
131        if ( $title ) {
132            $menu_item[0] = $title;
133            $menu_item[3] = esc_attr( $title );
134        }
135
136        if ( $cap ) {
137            $menu_item[1] = $cap;
138        }
139
140        // Change parent slug only if there are no submenus (the slug of the 1st submenu will be used if there are submenus).
141        if ( $url ) {
142            $this->hide_submenu_page( $slug, $slug );
143
144            if ( ! isset( $submenu[ $slug ] ) || ! $this->has_visible_items( $submenu[ $slug ] ) ) {
145                $menu_item[2] = $url;
146            }
147        }
148
149        if ( $icon ) {
150            $menu_item[4] = 'menu-top';
151            $menu_item[6] = $icon;
152        }
153
154        unset( $menu[ $menu_position ] );
155        if ( $position ) {
156            $menu_position = $position;
157        }
158        $this->set_menu_item( $menu_item, $menu_position );
159
160        // Only add submenu when there are other submenu items.
161        if ( $url && isset( $submenu[ $slug ] ) && $this->has_visible_items( $submenu[ $slug ] ) ) {
162            // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
163            add_submenu_page( $slug, $menu_item[3], $menu_item[0], $menu_item[1], $url, null, 0 );
164        }
165
166        return true;
167    }
168
169    /**
170     * Updates the submenus of the given menu slug.
171     *
172     * It hides the menu by adding the `hide-if-js` css class and duplicates the submenu with the new slug.
173     *
174     * @param string $slug Menu slug.
175     * @param array  $submenus_to_update Array of new submenu slugs.
176     */
177    public function update_submenus( $slug, $submenus_to_update ) {
178        global $submenu;
179
180        if ( ! isset( $submenu[ $slug ] ) ) {
181            return;
182        }
183
184        // This is needed for cases when the submenus to update have the same new slug.
185        $submenus_to_update = array_filter(
186            $submenus_to_update,
187            static function ( $item, $old_slug ) {
188                return $item !== $old_slug;
189            },
190            ARRAY_FILTER_USE_BOTH
191        );
192
193        /**
194         * Iterate over all submenu items and add the hide the submenus with CSS classes.
195         * This is done separately of the second foreach because the position of the submenu might change.
196         */
197        foreach ( $submenu[ $slug ] as $index => $item ) {
198            if ( ! array_key_exists( $item[2], $submenus_to_update ) ) {
199                continue;
200            }
201
202            $this->hide_submenu_element( $index, $slug, $item );
203        }
204
205        $submenu_items = array_values( $submenu[ $slug ] );
206
207        /**
208         * Iterate again over the submenu array. We need a copy of the array because add_submenu_page will add new elements
209         * to submenu array that might cause an infinite loop.
210         */
211        foreach ( $submenu_items as $i => $submenu_item ) {
212            if ( ! array_key_exists( $submenu_item[2], $submenus_to_update ) ) {
213                continue;
214            }
215
216            add_submenu_page(
217                $slug,
218                $submenu_item[3] ?? '',
219                $submenu_item[0] ?? '',
220                $submenu_item[1] ?? 'read',
221                $submenus_to_update[ $submenu_item[2] ],
222                null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- Core should ideally document null for no-callback arg. https://core.trac.wordpress.org/ticket/52539.
223                0 === $i ? 0 : $i + 1
224            );
225        }
226    }
227
228    /**
229     * Adds a menu separator.
230     *
231     * @param int    $position The position in the menu order this item should appear.
232     * @param string $cap Optional. The capability required for this menu to be displayed to the user.
233     *                         Default: 'read'.
234     */
235    public function add_admin_menu_separator( $position = null, $cap = 'read' ) {
236        $menu_item = array(
237            '',                                  // Menu title (ignored).
238            $cap,                                // Required capability.
239            wp_unique_id( 'separator-custom-' ), // URL or file (ignored, but must be unique).
240            '',                                  // Page title (ignored).
241            'wp-menu-separator',                 // CSS class. Identifies this item as a separator.
242        );
243
244        $this->set_menu_item( $menu_item, $position );
245    }
246
247    /**
248     * Enqueues scripts and styles.
249     */
250    public function enqueue_scripts() {
251        $assets_base_path = '../../dist/admin-menu/';
252
253        Assets::register_script(
254            'jetpack-admin-menu',
255            $assets_base_path . 'admin-menu.js',
256            __FILE__,
257            array(
258                'enqueue'  => true,
259                'css_path' => $assets_base_path . 'admin-menu.css',
260            )
261        );
262
263        wp_localize_script(
264            'jetpack-admin-menu',
265            'jetpackAdminMenu',
266            array(
267                'jitmDismissNonce' => wp_create_nonce( 'jitm_dismiss' ),
268            )
269        );
270
271        $this->configure_colors_for_rtl_stylesheets();
272    }
273
274    /**
275     * Mark the core colors stylesheets as RTL depending on the value from the environment.
276     * This fixes a core issue where the extra RTL data is not added to the colors stylesheet.
277     * https://core.trac.wordpress.org/ticket/53090
278     */
279    public function configure_colors_for_rtl_stylesheets() {
280        wp_style_add_data( 'colors', 'rtl', $this->is_rtl() );
281    }
282
283    /**
284     * Hide the submenu page based on slug and return the item that was hidden.
285     *
286     * Instead of actually removing the submenu item, a safer approach is to hide it and filter it in the API response.
287     * In this manner we'll avoid breaking third-party plugins depending on items that no longer exist.
288     *
289     * A false|array value is returned to be consistent with remove_submenu_page() function
290     *
291     * @param string $menu_slug The parent menu slug.
292     * @param string $submenu_slug The submenu slug that should be hidden.
293     * @return false|array
294     */
295    public function hide_submenu_page( $menu_slug, $submenu_slug ) {
296        global $submenu;
297
298        if ( ! isset( $submenu[ $menu_slug ] ) ) {
299            return false;
300        }
301
302        foreach ( $submenu[ $menu_slug ] as $i => $item ) {
303            if ( $submenu_slug !== $item[2] ) {
304                continue;
305            }
306
307            $this->hide_submenu_element( $i, $menu_slug, $item );
308
309            return $item;
310        }
311
312        return false;
313    }
314
315    /**
316     * Apply the hide-if-js CSS class to a submenu item.
317     *
318     * @param int    $index The position of a submenu item in the submenu array.
319     * @param string $parent_slug The parent slug.
320     * @param array  $item The submenu item.
321     */
322    public function hide_submenu_element( $index, $parent_slug, $item ) {
323        global $submenu;
324
325        $css_classes = empty( $item[4] ) ? self::HIDE_CSS_CLASS : $item[4] . ' ' . self::HIDE_CSS_CLASS;
326
327        // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
328        $submenu [ $parent_slug ][ $index ][4] = $css_classes;
329    }
330
331    /**
332     * Check if the menu has submenu items visible
333     *
334     * @param array $submenu_items The submenu items.
335     * @return bool
336     */
337    public function has_visible_items( $submenu_items ) {
338        $visible_items = array_filter(
339            $submenu_items,
340            array( $this, 'is_item_visible' )
341        );
342
343        return array() !== $visible_items;
344    }
345
346    /**
347     * Return the number of existing submenu items under the supplied parent slug.
348     *
349     * @param string $parent_slug The slug of the parent menu.
350     * @return int The number of submenu items under $parent_slug.
351     */
352    public function get_submenu_item_count( $parent_slug ) {
353        global $submenu;
354
355        if ( empty( $parent_slug ) || empty( $submenu[ $parent_slug ] ) || ! is_array( $submenu[ $parent_slug ] ) ) {
356            return 0;
357        }
358
359        return count( $submenu[ $parent_slug ] );
360    }
361
362    /**
363     * Adds the given menu item in the specified position.
364     *
365     * @param array $item The menu item to add.
366     * @param int   $position The position in the menu order this item should appear.
367     */
368    public function set_menu_item( $item, $position = null ) {
369        global $menu;
370
371        // Handle position (avoids overwriting menu items already populated in the given position).
372        // Inspired by https://core.trac.wordpress.org/browser/trunk/src/wp-admin/menu.php?rev=49837#L160.
373        if ( null === $position ) {
374            $menu[] = $item; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
375        } elseif ( isset( $menu[ "$position" ] ) ) {
376            $position           += (int) substr( base_convert( md5( $item[2] . $item[0] ), 16, 10 ), -5 ) * 0.00001;
377            $menu[ "$position" ] = $item; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
378        } else {
379            $menu[ $position ] = $item; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
380        }
381    }
382
383    /**
384     * Determines whether the current locale is right-to-left (RTL).
385     */
386    public function is_rtl() {
387        return is_rtl();
388    }
389
390    /**
391     * Checks for any SVG icons in the menu, and overrides things so that
392     * we can display the icon in the correct colour for the theme.
393     */
394    public function override_svg_icons() {
395        global $menu;
396
397        $svg_items = array();
398        foreach ( $menu as $idx => $menu_item ) {
399            // Menu items that don't have icons, for example separators, have less than 7
400            // elements, partly because the 7th is the icon. So, if we have less than 7,
401            // let's skip it.
402            if ( ! is_countable( $menu_item ) || ( count( $menu_item ) < 7 ) ) {
403                continue;
404            }
405
406            // If the hookname contain a URL than sanitize it by replacing invalid characters.
407            if ( str_contains( $menu_item[5], '://' ) ) {
408                $menu_item[5] = preg_replace( '![:/.]+!', '_', $menu_item[5] );
409            }
410
411            $menu_item[5] = preg_replace( '|[^a-zA-Z0-9_:.]|', '-', $menu_item[5] );
412
413            if ( str_starts_with( $menu_item[6], 'data:image/svg+xml' ) && 'site-card' !== $menu_item[3] ) {
414                $svg_items[]   = array(
415                    'icon' => $menu_item[6],
416                    'id'   => $menu_item[5],
417                );
418                $menu_item[4] .= ' menu-svg-icon';
419                $menu_item[6]  = 'none';
420            }
421            // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
422            $menu[ $idx ] = $menu_item;
423        }
424        if ( $svg_items !== array() ) {
425            $styles = '.menu-svg-icon .wp-menu-image { background-repeat: no-repeat; background-position: center center } ';
426            foreach ( $svg_items as $svg_item ) {
427                $styles .= sprintf( '#%s .wp-menu-image { background-image: url( "%s" ) }', $svg_item['id'], $svg_item['icon'] );
428            }
429            $styles .= '@supports ( mask-image: none ) or ( -webkit-mask-image: none ) { ';
430            $styles .= '.menu-svg-icon .wp-menu-image { background-image: none; } ';
431            $styles .= '.menu-svg-icon .wp-menu-image::before { background-color: currentColor; ';
432            $styles .= 'mask-size: contain; mask-position: center center; mask-repeat: no-repeat; ';
433            $styles .= '-webkit-mask-size: contain; -webkit-mask-position: center center; -webkit-mask-repeat: no-repeat; content:"" } ';
434            foreach ( $svg_items as $svg_item ) {
435                $styles .= sprintf(
436                    '#%s .wp-menu-image { background-image: none; } #%s .wp-menu-image::before{ mask-image: url( "%s" ); -webkit-mask-image: url( "%s" ) }',
437                    $svg_item['id'],
438                    $svg_item['id'],
439                    $svg_item['icon'],
440                    $svg_item['icon']
441                );
442            }
443            $styles .= '}';
444
445            wp_register_style( 'svg-menu-overrides', false, array(), '20210331' );
446            wp_enqueue_style( 'svg-menu-overrides' );
447            wp_add_inline_style( 'svg-menu-overrides', $styles );
448        }
449    }
450
451    /**
452     * Hide menus that are unauthorized and don't have visible submenus and cases when the menu has the same slug
453     * as the first submenu item.
454     *
455     * This must be done at the end of menu and submenu manipulation in order to avoid performing this check each time
456     * the submenus are altered.
457     */
458    public function hide_parent_of_hidden_submenus() {
459        global $menu, $submenu;
460
461        $this->sort_hidden_submenus();
462
463        foreach ( $menu as $menu_index => $menu_item ) {
464            // Skip if the menu doesn't have submenus.
465            if ( empty( $submenu[ $menu_item[2] ] ) || ! is_array( $submenu[ $menu_item[2] ] ) ) {
466                continue;
467            }
468
469            // If the first submenu item is hidden then we should also hide the parent.
470            // Since the submenus are ordered by self::HIDE_CSS_CLASS (hidden submenus should be at the end of the array),
471            // we can say that if the first submenu is hidden then we should also hide the menu.
472            $first_submenu_item       = array_values( $submenu[ $menu_item[2] ] )[0];
473            $is_first_submenu_visible = $this->is_item_visible( $first_submenu_item );
474
475            // if the user does not have access to the menu and the first submenu is hidden, then hide the menu.
476            if ( ! current_user_can( $menu_item[1] ) && ! $is_first_submenu_visible ) {
477                // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
478                $menu[ $menu_index ][4] = self::HIDE_CSS_CLASS;
479            }
480
481            // if the menu has the same slug as the first submenu then hide the submenu.
482            if ( $menu_item[2] === $first_submenu_item[2] && ! $is_first_submenu_visible ) {
483                // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
484                $menu[ $menu_index ][4] = self::HIDE_CSS_CLASS;
485            }
486        }
487    }
488
489    /**
490     * Sort the hidden submenus by moving them at the end of the array in order to avoid WP using them as default URLs.
491     *
492     * This operation has to be done at the end of submenu manipulation in order to guarantee that the hidden submenus
493     * are at the end of the array.
494     */
495    public function sort_hidden_submenus() {
496        global $submenu;
497
498        foreach ( $submenu as $menu_slug => $submenu_items ) {
499            if ( ! $submenu_items ) {
500                continue;
501            }
502
503            foreach ( $submenu_items as $submenu_index => $submenu_item ) {
504                if ( $this->is_item_visible( $submenu_item ) ) {
505                    continue;
506                }
507
508                unset( $submenu[ $menu_slug ][ $submenu_index ] );
509                // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
510                $submenu[ $menu_slug ][] = $submenu_item;
511            }
512        }
513    }
514
515    /**
516     * Check if the given item is visible or not in the admin menu.
517     *
518     * @param array $item A menu or submenu array.
519     */
520    public function is_item_visible( $item ) {
521        return ! isset( $item[4] ) || ! str_contains( $item[4], self::HIDE_CSS_CLASS );
522    }
523
524    /**
525     * Adds a dashboard switcher to the list of screen meta links of the current page.
526     */
527    public function add_dashboard_switcher() {
528        $menu_mappings = require __DIR__ . '/menu-mappings.php';
529        $screen        = $this->get_current_screen();
530
531        // Let's show the switcher only in screens that we have a Calypso mapping to switch to.
532        if ( empty( $menu_mappings[ $screen ] ) ) {
533            return;
534        }
535        ?>
536        <div id="view-link-wrap" class="hide-if-no-js screen-meta-toggle">
537            <button type="button" id="view-link" class="button show-settings" aria-expanded="false"><?php echo esc_html_x( 'View', 'View options to switch between', 'jetpack-masterbar' ); ?></button>
538        </div>
539        <div id="view-wrap" class="screen-options-tab__wrapper hide-if-no-js hidden" tabindex="-1">
540            <div class="screen-options-tab__dropdown" data-testid="screen-options-dropdown">
541                <div class="screen-switcher">
542                    <a class="screen-switcher__button" href="<?php echo esc_url( add_query_arg( 'preferred-view', 'default' ) ); ?>" data-view="default">
543                        <strong><?php esc_html_e( 'Default view', 'jetpack-masterbar' ); ?></strong>
544                        <?php esc_html_e( 'Our WordPress.com redesign for a better experience.', 'jetpack-masterbar' ); ?>
545                    </a>
546                    <button class="screen-switcher__button"  data-view="classic">
547                        <strong><?php esc_html_e( 'Classic view', 'jetpack-masterbar' ); ?></strong>
548                        <?php esc_html_e( 'The classic WP-Admin WordPress interface.', 'jetpack-masterbar' ); ?>
549                    </button>
550                </div>
551            </div>
552        </div>
553        <?php
554    }
555
556    /**
557     * Adds a script to append the dashboard switcher to screen meta
558     */
559    public function dashboard_switcher_scripts() {
560        wp_add_inline_script(
561            'common',
562            "(function( $ ) {
563                $( '#view-link-wrap' ).appendTo( '#screen-meta-links' );
564
565                var viewLink = $( '#view-link' );
566                var viewWrap = $( '#view-wrap' );
567
568                viewLink.on( 'click', function() {
569                    viewWrap.toggle();
570                    viewLink.toggleClass( 'screen-meta-active' );
571                } );
572
573                $( document ).on( 'mouseup', function( event ) {
574                    if ( ! viewLink.is( event.target ) && ! viewWrap.is( event.target ) && viewWrap.has( event.target ).length === 0 ) {
575                        viewWrap.hide();
576                        viewLink.removeClass( 'screen-meta-active' );
577                    }
578                });
579            })( jQuery );"
580        );
581    }
582
583    /**
584     * Sets the given view as preferred for the givens screen.
585     *
586     * @param string $screen Screen identifier.
587     * @param string $view Preferred view.
588     */
589    public function set_preferred_view( $screen, $view ) {
590        remove_filter( 'get_user_option_jetpack_admin_menu_preferred_views', 'wpcom_admin_get_user_option_jetpack' );
591        $preferred_views = $this->get_preferred_views();
592        if ( function_exists( 'wpcom_admin_get_user_option_jetpack' ) ) {
593            add_filter( 'get_user_option_jetpack_admin_menu_preferred_views', 'wpcom_admin_get_user_option_jetpack' );
594        }
595
596        $screen                     = str_replace( '?post_type=post', '', $screen );
597        $preferred_views[ $screen ] = $view;
598        update_user_option( get_current_user_id(), 'jetpack_admin_menu_preferred_views', $preferred_views );
599    }
600
601    /**
602     * Get the preferred views for all screens.
603     *
604     * @return array
605     */
606    public function get_preferred_views() {
607        $preferred_views = get_user_option( 'jetpack_admin_menu_preferred_views' );
608
609        if ( ! $preferred_views ) {
610            return array();
611        }
612
613        return $preferred_views;
614    }
615
616    /**
617     * Get the preferred view for the given screen.
618     *
619     * @param string $screen Screen identifier.
620     * @param bool   $fallback_global_preference (Optional) Whether the global preference for all screens should be used
621     *                                           as fallback if there is no specific preference for the given screen.
622     *                                           Default: true.
623     * @return string
624     */
625    public function get_preferred_view( $screen, $fallback_global_preference = true ) {
626        $preferred_views = $this->get_preferred_views();
627
628        if ( ! isset( $preferred_views[ $screen ] ) ) {
629            if ( ! $fallback_global_preference ) {
630                return self::UNKNOWN_VIEW;
631            }
632
633            $should_link_to_wp_admin = $this->should_link_to_wp_admin() || $this->use_wp_admin_interface();
634            return $should_link_to_wp_admin ? self::CLASSIC_VIEW : self::DEFAULT_VIEW;
635        }
636
637        return $preferred_views[ $screen ];
638    }
639
640    /**
641     * Gets the identifier of the current screen.
642     *
643     * @return string
644     */
645    public function get_current_screen() {
646        // phpcs:disable WordPress.Security.NonceVerification
647        global $pagenow;
648        $screen = isset( $_REQUEST['screen'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['screen'] ) ) : $pagenow;
649        if ( isset( $_GET['post_type'] ) ) {
650            $screen = add_query_arg( 'post_type', sanitize_text_field( wp_unslash( $_GET['post_type'] ) ), $screen );
651        }
652        if ( isset( $_GET['taxonomy'] ) ) {
653            $screen = add_query_arg( 'taxonomy', sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) ), $screen );
654        }
655        if ( isset( $_GET['page'] ) ) {
656            $screen = add_query_arg( 'page', sanitize_text_field( wp_unslash( $_GET['page'] ) ), $screen );
657        }
658        return $screen;
659        // phpcs:enable WordPress.Security.NonceVerification
660    }
661
662    /**
663     * Stores the preferred view for the current screen.
664     */
665    public function handle_preferred_view() {
666        // phpcs:disable WordPress.Security.NonceVerification
667        if ( ! isset( $_GET['preferred-view'] ) ) {
668            return;
669        }
670
671        // phpcs:disable WordPress.Security.NonceVerification
672        $preferred_view = sanitize_key( $_GET['preferred-view'] );
673
674        if ( ! in_array( $preferred_view, array( self::DEFAULT_VIEW, self::CLASSIC_VIEW ), true ) ) {
675            return;
676        }
677
678        $current_screen = $this->get_current_screen();
679
680        $this->set_preferred_view( $current_screen, $preferred_view );
681
682        /**
683         * Dashboard Quick switcher action triggered when a user switches to a different view.
684         *
685         * @module masterbar
686         *
687         * @since jetpack-9.9.1
688         *
689         * @param string The current screen of the user.
690         * @param string The preferred view the user selected.
691         */
692        \do_action( 'jetpack_dashboard_switcher_changed_view', $current_screen, $preferred_view );
693
694        if ( self::DEFAULT_VIEW === $preferred_view ) {
695            // Redirect to default view if that's the newly preferred view.
696            $menu_mappings = require __DIR__ . '/menu-mappings.php';
697            if ( isset( $menu_mappings[ $current_screen ] ) ) {
698                // Using `wp_redirect` intentionally because we're redirecting to Calypso.
699                wp_redirect( $menu_mappings[ $current_screen ] . $this->domain ); // phpcs:ignore WordPress.Security.SafeRedirect
700                exit( 0 );
701            }
702        } elseif ( self::CLASSIC_VIEW === $preferred_view ) {
703            // Removes the `preferred-view` param from the URL to avoid issues with
704            // screens that don't expect this param to be present in the URL.
705            wp_safe_redirect( remove_query_arg( 'preferred-view' ) );
706            exit( 0 );
707        }
708        // phpcs:enable WordPress.Security.NonceVerification
709    }
710
711    /**
712     * Adds the necessary CSS class to the admin body class.
713     *
714     * @param string $admin_body_classes Contains all the admin body classes.
715     *
716     * @return string
717     */
718    public function admin_body_class( $admin_body_classes ) {
719        return " is-nav-unification $admin_body_classes ";
720    }
721
722    /**
723     * Whether to use wp-admin pages rather than Calypso.
724     *
725     * Options:
726     * false - Calypso (Default).
727     * true  - wp-admin.
728     *
729     * @return bool
730     */
731    public function should_link_to_wp_admin() {
732        return get_user_option( 'jetpack_admin_menu_link_destination' );
733    }
734
735    /**
736     * Whether the current user has indicated they want to use the wp-admin interface for the given screen.
737     *
738     * @return bool
739     */
740    public function use_wp_admin_interface() {
741        return 'wp-admin' === get_option( 'wpcom_admin_interface' );
742    }
743
744    /**
745     * Create the desired menu output.
746     */
747    abstract public function reregister_menu_items();
748}