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