PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
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 13.7.2 All 507 releases
← All changes | jetpack_vendor/automattic/jetpack-admin-ui/src/class-admin-menu.php +192 -41 16.3-a.1 → 16.3-beta View file →
@@ -6,8 +6,9 @@
6 6 */
7 7
8 8 namespace Automattic\Jetpack\Admin_UI;
9 9
10 +use Automattic\Jetpack\Feature_Policy;
10 11 use Automattic\Jetpack\Tracking;
11 12 use Jetpack_Options;
12 13 use Jetpack_Tracks_Client;
13 14
@@ -16,9 +17,9 @@
16 17 * If the Jetpack top level was not previously registered by other plugin, it will be registered here.
17 18 */
18 19 class Admin_Menu {
19 20
20 - const PACKAGE_VERSION = '0.12.0';
21 + const PACKAGE_VERSION = '0.14.3';
21 22
22 23 /**
23 24 * Slug used for the upgrade menu item and redirect URL.
24 25 *
@@ -36,10 +37,10 @@
36 37 const UPGRADE_MENU_FALLBACK_URL = 'https://jetpack.com/upgrade/';
37 38
38 39 /*
39 40 * The sidebar's position tiers. Items sharing a tier sort alphabetically by menu title, so a
40 - * product should pass no position and land in POSITION_DEFAULT. Reach for another tier only
41 - * for one of the roles below; an int of your own silently opts the item out of that order.
41 + * product should pass no position and land in POSITION_DEFAULT. add_menu() treats any value
42 + * that is not a tier below as omitted, so an int of your own cannot opt an item out.
42 43 */
43 44
44 45 /**
45 46 * Owns the top-level Jetpack link, since WordPress points it at whichever item sorts first.
@@ -83,14 +84,25 @@
83 84 */
84 85 const POSITION_UPGRADE = 999;
85 86
86 87 /**
87 - * Handle for the shared, token-only WPDS design-tokens stylesheet.
88 + * The tiers add_menu() accepts. POSITION_UPGRADE is left out: only this class claims it.
88 89 *
89 - * Registered once and enqueued on every Jetpack admin page so that
90 - * `var(--wpds-*)` values resolve at runtime instead of falling back to
91 - * their hand-written hex defaults.
90 + * @var int[]
91 + */
92 + private const CALLER_POSITIONS = array(
93 + self::POSITION_FIRST,
94 + self::POSITION_FIRST_FALLBACK,
95 + self::POSITION_DEFAULT,
96 + self::POSITION_EXTERNAL,
97 + self::POSITION_LAST,
98 + );
99 +
100 + /**
101 + * Handle for the bundled WPDS design-tokens stylesheet.
92 102 *
103 + * Fallback when Core/Gutenberg has not registered the `wp-theme` style.
104 + *
93 105 * @var string
94 106 */
95 107 const DESIGN_TOKENS_HANDLE = 'jetpack-admin-ui-design-tokens';
96 108
@@ -136,8 +148,15 @@
136 148 */
137 149 private static $menu_items = array();
138 150
139 151 /**
152 + * List of top level menu items enqueued to be added
153 + *
154 + * @var array
155 + */
156 + private static $top_level_items = array();
157 +
158 + /**
140 159 * Hook suffixes of the pages registered through this class.
141 160 *
142 161 * Used to scope the design-tokens stylesheet to Jetpack admin pages.
143 162 *
@@ -169,8 +188,24 @@
169 188 */
170 189 private static $hidden_menu_slugs = array();
171 190
172 191 /**
192 + * Top level menu slugs registered this request but kept out of the rendered sidebar.
193 + *
194 + * @var string[]
195 + */
196 + private static $hidden_top_level_slugs = array();
197 +
198 + /**
199 + * Whether the top level registration pass has been hooked.
200 + *
201 + * Separate from $initialized, which also builds the Jetpack menu.
202 + *
203 + * @var boolean
204 + */
205 + private static $top_level_initialized = false;
206 +
207 + /**
173 208 * Initialize the class and set up the main hook
174 209 *
175 210 * @return void
176 211 */
@@ -186,8 +221,32 @@
186 221 }
187 222 }
188 223
189 224 /**
225 + * Drops every queued item and unhooks the registration passes.
226 + *
227 + * Intended for tests.
228 + *
229 + * @return void
230 + */
231 + public static function reset() {
232 + self::$menu_items = array();
233 + self::$top_level_items = array();
234 + self::$page_hooks = array();
235 + self::$hidden_menu_slugs = array();
236 + self::$hidden_top_level_slugs = array();
237 + self::$initialized = false;
238 + self::$top_level_initialized = false;
239 +
240 + remove_action( 'admin_menu', array( __CLASS__, 'admin_menu_hook_callback' ), 1000 );
241 + remove_action( 'network_admin_menu', array( __CLASS__, 'admin_menu_hook_callback' ), 1000 );
242 + remove_action( 'admin_menu', array( __CLASS__, 'top_level_menu_hook_callback' ), 1000 );
243 + remove_action( 'admin_head', array( __CLASS__, 'remove_hidden_menu_items' ) );
244 + remove_action( 'admin_enqueue_scripts', array( __CLASS__, 'add_upgrade_menu_item_styles' ) );
245 + remove_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_design_tokens' ) );
246 + }
247 +
248 + /**
190 249 * Handles the Akismet menu item when used alongside other stand-alone plugins
191 250 *
192 251 * When Jetpack plugin is present, Akismet menu item is moved under the Jetpack top level menu, but if Akismet is active alongside other stand-alone plugins,
193 252 * we use this method to move the menu item.
@@ -238,14 +297,11 @@
238 297 // If Jetpack plugin is not present, user will only be able to see this menu if they have enough capability to at least one of the sub menus being added.
239 298 $can_see_toplevel_menu = false;
240 299 }
241 300
242 - /**
243 - * The add_sub_menu function has a bug and will not keep the right order of menu items.
244 - *
245 - * @see https://core.trac.wordpress.org/ticket/52035
246 - * Let's order the items before registering them.
247 - * Since this all happens after the Jetpack plugin menu items were added, all items will be added after Jetpack plugin items - unless position is very low number (smaller than the number of menu items present in Jetpack plugin).
301 + /*
302 + * Sort here and register in that order, without passing the position on: core splices an
303 + * int back in, and prepends 0 or less. See https://core.trac.wordpress.org/ticket/52035.
248 304 */
249 305 usort(
250 306 self::$menu_items,
251 307 function ( $a, $b ) {
@@ -287,10 +343,9 @@
287 343 $menu_item['page_title'],
288 344 $menu_item['menu_title'],
289 345 $menu_item['capability'],
290 346 $menu_item['menu_slug'],
291 - $menu_item['function'],
292 - $menu_item['position']
347 + $menu_item['function']
293 348 );
294 349 }
295 350
296 351 if ( ! $jetpack_plugin_present ) {
@@ -304,8 +359,82 @@
304 359 self::maybe_add_upgrade_menu_item();
305 360 }
306 361
307 362 /**
363 + * Hooks the top level registration pass, without building the Jetpack menu that init() does.
364 + *
365 + * @return void
366 + */
367 + private static function init_top_level() {
368 + if ( ! self::$top_level_initialized ) {
369 + self::$top_level_initialized = true;
370 + add_action( 'admin_menu', array( __CLASS__, 'top_level_menu_hook_callback' ), 1000 );
371 + add_action( 'admin_head', array( __CLASS__, 'remove_hidden_menu_items' ) );
372 + }
373 + }
374 +
375 + /**
376 + * Registers the queued top level items, skipping the ones that should not be seen.
377 + *
378 + * These sit beside the Jetpack menu, so they never count towards keeping it alive.
379 + *
380 + * @return void
381 + */
382 + public static function top_level_menu_hook_callback() {
383 + $visibility = self::get_visibility_states();
384 +
385 + self::$hidden_top_level_slugs = array();
386 +
387 + foreach ( self::$top_level_items as $menu_item ) {
388 + if ( ! current_user_can( $menu_item['capability'] ) ) {
389 + continue;
390 + }
391 +
392 + if ( ! self::is_menu_item_visible( $menu_item, $visibility ) ) {
393 + self::$hidden_top_level_slugs[] = $menu_item['menu_slug'];
394 + }
395 +
396 + add_menu_page(
397 + $menu_item['page_title'],
398 + $menu_item['menu_title'],
399 + $menu_item['capability'],
400 + $menu_item['menu_slug'],
401 + $menu_item['function'],
402 + $menu_item['icon_url'],
403 + $menu_item['position']
404 + );
405 + }
406 + }
407 +
408 + /**
409 + * Adds a top level menu item under the same visibility gate and filter as add_menu().
410 + *
411 + * Unlike add_menu(), the page gets neither the core-notice CSS nor the design tokens.
412 + * Parameters mirror add_menu_page(), with $args appended.
413 + *
414 + * @since 0.13.0
415 + *
416 + * @param string $page_title The text to be displayed in the title tags of the page when the menu
417 + * is selected.
418 + * @param string $menu_title The text to be used for the menu.
419 + * @param string $capability The capability required for this menu to be displayed to the user.
420 + * @param string $menu_slug The slug name to refer to this menu by. Should be unique for this menu.
421 + * @param callable|null $function The function to be called to output the content for this page.
422 + * @param string $icon_url The URL to the icon to be used for this menu, or a dashicons class.
423 + * @param int|null $position The position in the menu order this item should appear.
424 + * @param array $args Optional. Visibility declaration for this item; see add_menu().
425 + *
426 + * @return string The resulting page's hook_suffix
427 + */
428 + public static function add_top_level_menu( $page_title, $menu_title, $capability, $menu_slug, $function, $icon_url = '', $position = null, $args = array() ) {
429 + self::init_top_level();
430 + self::$top_level_items[] = compact( 'page_title', 'menu_title', 'capability', 'menu_slug', 'function', 'icon_url', 'position', 'args' );
431 +
432 + // Same derivation as get_plugin_page_hookname(), which strips ".php" anywhere in the slug.
433 + return 'toplevel_page_' . preg_replace( '!\.php!', '', plugin_basename( $menu_slug ) );
434 + }
435 +
436 + /**
308 437 * Adds a new submenu to the Jetpack Top level menu
309 438 *
310 439 * The parameters this method accepts are the same as @see add_submenu_page. This class will
311 440 * aggreagate all menu items registered by stand-alone plugins and make sure they all go under the same
@@ -318,9 +447,9 @@
318 447 * @param string $menu_slug The slug name to refer to this menu by. Should be unique for this menu
319 448 * and only include lowercase alphanumeric, dashes, and underscores characters
320 449 * to be compatible with sanitize_key().
321 450 * @param callable|null $function The function to be called to output the content for this page.
322 - * @param int|null $position The position in the menu order this item should appear. Leave empty typically.
451 + * @param int|null $position One of the POSITION_* tiers; any other value is ignored. Leave empty typically.
323 452 * @param array $args Optional. Visibility declaration for this item:
324 453 * - 'product' (string) My Jetpack product slug whose activation gates the item.
325 454 * - 'module' (string) Jetpack module name, for items with no product class.
326 455 * - 'key' (string) The name hosts use for this item in the visibility
@@ -330,25 +459,36 @@
330 459 * @return string The resulting page's hook_suffix
331 460 */
332 461 public static function add_menu( $page_title, $menu_title, $capability, $menu_slug, $function, $position = null, $args = array() ) {
333 462 self::init();
463 +
464 + /*
465 + * Anything but a tier would opt the item out of the alphabetical order, so treat it as omitted.
466 + * @todo Add _doing_it_wrong() here after December 2026. Until all our own plugins ship tier-only
467 + * positions, it would have the latest release of one Jetpack plugin warning about another.
468 + */
469 + $position = is_numeric( $position ) && in_array( (int) $position, self::CALLER_POSITIONS, true ) ? (int) $position : null;
470 +
334 471 self::$menu_items[] = compact( 'page_title', 'menu_title', 'capability', 'menu_slug', 'function', 'position', 'args' );
335 472
336 473 /**
337 474 * Let's return the page hook so consumers can use.
338 - * We know all pages will be under Jetpack top level menu page, so we can hardcode the first part of the string.
475 + * Pages normally sit under the Jetpack top level menu page, so we can hardcode the first part of the string.
339 476 * Using get_plugin_page_hookname here won't work because the top level page is not registered yet.
340 477 */
341 478 $hook = 'jetpack_page_' . $menu_slug;
342 479
343 - // Track the page hook so the design-tokens stylesheet can be scoped to it.
344 - self::$page_hooks[] = $hook;
480 + // Core names the page admin_page_<slug> instead when the user has no Jetpack top-level menu.
481 + foreach ( array( $hook, 'admin_page_' . $menu_slug ) as $page_hook ) {
482 + // Track the page hook so the design-tokens stylesheet can be scoped to it.
483 + self::$page_hooks[] = $page_hook;
345 484
346 - // Hide WordPress core admin notices on this Jetpack page. The load-<hook>
347 - // action only fires when the matching screen is being rendered, so this
348 - // stays scoped to Jetpack pages and reaches every page registered here.
349 - add_action( 'load-' . $hook, array( __CLASS__, 'hide_core_admin_notices' ) );
350 - add_action( 'load-' . $hook . '-network', array( __CLASS__, 'hide_core_admin_notices' ) );
485 + // Hide WordPress core admin notices on this Jetpack page. The load-<hook>
486 + // action only fires when the matching screen is being rendered, so this
487 + // stays scoped to Jetpack pages and reaches every page registered here.
488 + add_action( 'load-' . $page_hook, array( __CLASS__, 'hide_core_admin_notices' ) );
489 + add_action( 'load-' . $page_hook . '-network', array( __CLASS__, 'hide_core_admin_notices' ) );
490 + }
351 491
352 492 return $hook;
353 493 }
354 494
@@ -427,12 +567,12 @@
427 567 self::$visibility_resolver = $resolver;
428 568 }
429 569
430 570 /**
431 - * Takes hidden items out of the Jetpack submenu before the sidebar renders.
571 + * Takes hidden items out of the sidebar before it renders.
432 572 *
433 - * Runs on admin_head, after core's access check has already resolved the current page
434 - * against $submenu, so a hidden item's page stays reachable while its entry disappears.
573 + * Runs on admin_head, after core's access check has already resolved the current page,
574 + * so a hidden item's page stays reachable while its entry disappears.
435 575 *
436 576 * @return void
437 577 */
438 578 public static function remove_hidden_menu_items() {
@@ -438,8 +578,12 @@
438 578 public static function remove_hidden_menu_items() {
439 579 foreach ( self::$hidden_menu_slugs as $menu_slug ) {
440 580 remove_submenu_page( 'jetpack', plugin_basename( $menu_slug ) );
441 581 }
582 +
583 + foreach ( self::$hidden_top_level_slugs as $menu_slug ) {
584 + remove_menu_page( plugin_basename( $menu_slug ) );
585 + }
442 586 }
443 587
444 588 /**
445 589 * Returns the name a host uses for a menu item in the visibility filter.
@@ -465,11 +609,17 @@
465 609 *
466 610 * @return array Map of item key to one of the VISIBILITY_* states.
467 611 */
468 612 private static function get_visibility_states() {
613 + // This filter is one a policy feeds, and nothing else need have read the policy this request.
614 + if ( method_exists( Feature_Policy::class, 'ensure_hooks' ) ) {
615 + Feature_Policy::ensure_hooks();
616 + }
617 +
469 618 $states = array();
619 + $items = array_merge( self::$menu_items, self::$top_level_items );
470 620
471 - foreach ( self::$menu_items as $menu_item ) {
621 + foreach ( $items as $menu_item ) {
472 622 $states[ self::get_item_key( $menu_item ) ] = self::VISIBILITY_DEFAULT;
473 623 }
474 624
475 625 /**
@@ -482,9 +632,9 @@
482 632 *
483 633 * @param array $states Map of item key (menu slug unless the item declared one) to state.
484 634 * @param array $menu_items The registered menu items, for context.
485 635 */
486 - $states = apply_filters( 'jetpack_admin_menu_visibility', $states, self::$menu_items );
636 + $states = apply_filters( 'jetpack_admin_menu_visibility', $states, $items );
487 637
488 638 return is_array( $states ) ? $states : array();
489 639 }
490 640
@@ -772,31 +922,32 @@
772 922 self::enqueue_upgrade_menu_tracks_script( $asset );
773 923 }
774 924
775 925 /**
776 - * Enqueues the shared, token-only WPDS design-tokens stylesheet.
926 + * Enqueues WPDS design tokens so `var(--wpds-*)` values resolve at runtime.
777 927 *
778 - * Single entry point for any consumer that needs WPDS `var(--wpds-*)` values
779 - * to resolve at runtime on a Jetpack admin page. Registers the handle on
780 - * first use (idempotent) and enqueues it; the caller is responsible for
781 - * scoping the call to the right page(s). Since admin-ui is a dependency of
782 - * the Jetpack plugin and the modernized packages, both the plugin's
783 - * legacy/wrap_ui gate and this package's own dashboards call through here,
784 - * so the handle has a single owner and there is no duplicated enqueue logic.
928 + * Prefer Core/Gutenberg's `wp-theme` style when registered; otherwise ship
929 + * the bundled copy. The caller scopes the call to the right page(s).
785 930 *
786 931 * @return void
787 932 */
788 933 public static function enqueue_design_tokens() {
934 + // Registered since WP 7.1 (and by Gutenberg):
935 + // https://make.wordpress.org/core/2026/07/31/design-system-theming-in-wordpress-7-1/
936 + if ( wp_style_is( 'wp-theme', 'registered' ) ) {
937 + wp_enqueue_style( 'wp-theme' );
938 + return;
939 + }
940 +
941 + // @todo Remove this, the called function, and the webpack entrypoint it registers when WP 7.1 is the minimum version.
789 942 self::register_design_tokens_style();
790 943 wp_enqueue_style( self::DESIGN_TOKENS_HANDLE );
791 944 }
792 945
793 946 /**
794 - * Registers the shared, token-only WPDS design-tokens stylesheet.
947 + * Registers the bundled, token-only WPDS design-tokens stylesheet.
795 948 *
796 - * The stylesheet only defines `:root{--wpds-*}` custom properties (no
797 - * component or class styles), giving every Jetpack admin page a single
798 - * runtime source for design tokens. It is safe to call repeatedly:
949 + * Used only when `wp-theme` is not registered. Safe to call repeatedly:
799 950 * wp_register_style() is a no-op once the handle is registered.
800 951 *
801 952 * @return void
802 953 */