PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-masterbar/src/admin-menu/class-base-admin-menu.php +748 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,748 @@
1 +<?php
2 +/**
3 + * Base Admin Menu file.
4 + *
5 + * @package automattic/jetpack-masterbar
6 + */
7 +
8 +namespace Automattic\Jetpack\Masterbar;
9 +
10 +use Automattic\Jetpack\Assets;
11 +use Automattic\Jetpack\Status;
12 +
13 +/**
14 + * Class Base_Admin_Menu
15 + */
16 +abstract 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 +}