PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-beta.3
Jetpack – WP Security, Backup, Speed, & Growth v16.1-beta.3
16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-masterbar / src / admin-menu / class-base-admin-menu.php
jetpack / jetpack_vendor / automattic / jetpack-masterbar / src / admin-menu Last commit date
class-admin-menu.php 5 months ago class-atomic-admin-menu.php 3 weeks ago class-base-admin-menu.php 2 months ago class-dashboard-switcher-tracking.php 2 years ago class-domain-only-admin-menu.php 9 months ago class-wpcom-admin-menu.php 5 months ago class-wpcom-email-subscription-checker.php 2 years ago dashicon-set.php 2 years ago globe-icon.svg 2 years ago load.php 9 months ago menu-mappings.php 9 months ago
class-base-admin-menu.php
749 lines
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 }
749