Skip to content

Commit 2ef0e86

Browse files
authored
Admin Menu: keep My Jetpack as the Jetpack menu link when Akismet is active (#53943)
1 parent 28343d1 commit 2ef0e86

8 files changed

Lines changed: 155 additions & 31 deletions

File tree

‎projects/packages/admin-ui/.phan/config.php‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,13 @@
1010
// Require base config.
1111
require __DIR__ . '/../../../../.phan/config.base.php';
1212

13-
return make_phan_config( dirname( __DIR__ ), array( '+stubs' => array( 'akismet' ) ) );
13+
return make_phan_config(
14+
dirname( __DIR__ ),
15+
array(
16+
'+stubs' => array( 'akismet' ),
17+
'exclude_file_list' => array(
18+
// Testing mock that overrides the akismet stubs.
19+
'tests/php/stubs/class-akismet-admin.php',
20+
),
21+
)
22+
);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fixed
3+
4+
Keep the Akismet Anti-spam item in sorted order when the menu is set up after Akismet registers it.

‎projects/packages/admin-ui/src/class-admin-menu.php‎

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ public static function reset() {
237237
self::$initialized = false;
238238
self::$top_level_initialized = false;
239239

240+
remove_action( 'admin_menu', array( __CLASS__, 'take_over_akismet_menu' ), 4 );
240241
remove_action( 'admin_menu', array( __CLASS__, 'admin_menu_hook_callback' ), 1000 );
241242
remove_action( 'network_admin_menu', array( __CLASS__, 'admin_menu_hook_callback' ), 1000 );
242243
remove_action( 'admin_menu', array( __CLASS__, 'top_level_menu_hook_callback' ), 1000 );
@@ -246,28 +247,37 @@ public static function reset() {
246247
}
247248

248249
/**
249-
* Handles the Akismet menu item when used alongside other stand-alone plugins
250+
* Registers the Akismet menu item through this class, so it sorts with the rest of the Jetpack menu.
250251
*
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,
252-
* we use this method to move the menu item.
252+
* Akismet hooks admin_menu at priority 5 and, when the Jetpack plugin is active, defers its entry to
253+
* jetpack_admin_menu, which fires before this class registers its sorted items.
253254
*/
254255
private static function handle_akismet_menu() {
255-
if ( class_exists( 'Akismet_Admin' ) ) {
256-
add_action(
257-
'admin_menu',
258-
function () {
259-
// Prevent Akismet from adding a menu item.
260-
remove_action( 'admin_menu', array( 'Akismet_Admin', 'admin_menu' ), 5 );
261-
262-
// Add an Anti-spam menu item for Jetpack.
263-
self::add_menu( __( 'Akismet Anti-spam', 'jetpack-admin-ui' ), __( 'Akismet Anti-spam', 'jetpack-admin-ui' ), 'manage_options', 'akismet-key-config', array( 'Akismet_Admin', 'display_page' ) );
264-
},
265-
4
266-
);
256+
if ( ! class_exists( 'Akismet_Admin' ) ) {
257+
return;
258+
}
267259

260+
// Initialized after priority 5, as when My Jetpack is the first caller: Akismet has already deferred.
261+
if ( has_action( 'jetpack_admin_menu', array( 'Akismet_Admin', 'load_menu' ) ) ) {
262+
self::take_over_akismet_menu();
263+
} else {
264+
add_action( 'admin_menu', array( __CLASS__, 'take_over_akismet_menu' ), 4 );
268265
}
269266
}
270267

268+
/**
269+
* Stops Akismet from adding its own menu item and queues ours in its place.
270+
*
271+
* @return void
272+
*/
273+
public static function take_over_akismet_menu() {
274+
// Older Jetpack plugins unhook Akismet's admin_menu callback themselves and rely on this item.
275+
remove_action( 'admin_menu', array( 'Akismet_Admin', 'admin_menu' ), 5 );
276+
remove_action( 'jetpack_admin_menu', array( 'Akismet_Admin', 'load_menu' ) );
277+
278+
self::add_menu( __( 'Akismet Anti-spam', 'jetpack-admin-ui' ), __( 'Akismet Anti-spam', 'jetpack-admin-ui' ), 'manage_options', 'akismet-key-config', array( 'Akismet_Admin', 'display_page' ) );
279+
}
280+
271281
/**
272282
* Callback to the admin_menu and network_admin_menu hooks that will register the enqueued menu items
273283
*

‎projects/packages/admin-ui/tests/php/Admin_Menu_Test.php‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
use Automattic\Jetpack\Feature_Policy;
1111
use PHPUnit\Framework\Attributes\DataProvider;
12+
use PHPUnit\Framework\Attributes\PreserveGlobalState;
13+
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
1214
use PHPUnit\Framework\TestCase;
1315

1416
/**
@@ -406,6 +408,85 @@ function ( $states ) {
406408
);
407409
}
408410

411+
/**
412+
* Akismet's item sorts below POSITION_FIRST however Admin_Menu and Akismet are ordered.
413+
*
414+
* Runs alone because the Akismet_Admin stub, once loaded, would add Akismet to every other test's menu.
415+
*
416+
* @param string $order Which side hooks in first; see akismet_order_data().
417+
*
418+
* @dataProvider akismet_order_data
419+
* @runInSeparateProcess
420+
* @preserveGlobalState disabled
421+
*/
422+
#[DataProvider( 'akismet_order_data' )]
423+
#[RunInSeparateProcess]
424+
#[PreserveGlobalState( false )]
425+
public function test_akismet_item_sorts_below_the_first_item( $order ) {
426+
global $submenu;
427+
428+
require_once __DIR__ . '/stubs/class-akismet-admin.php';
429+
430+
// PHPUnit 9 does not run setUpBeforeClass() in the child process, so make the admin here.
431+
wp_set_current_user(
432+
wp_insert_user(
433+
array(
434+
'user_login' => 'akismet_order_admin',
435+
'user_pass' => 'pass',
436+
'user_email' => 'akismet_order_admin@example.com',
437+
'role' => 'administrator',
438+
)
439+
)
440+
);
441+
442+
// The Jetpack plugin fires jetpack_admin_menu at 998, before Admin_Menu registers at 1000.
443+
add_action(
444+
'admin_menu',
445+
function () {
446+
do_action( 'jetpack_admin_menu' );
447+
},
448+
998
449+
);
450+
451+
if ( 'akismet-first' === $order ) {
452+
\Akismet_Admin::admin_menu();
453+
} else {
454+
add_action( 'admin_menu', array( 'Akismet_Admin', 'admin_menu' ), 5 );
455+
}
456+
457+
if ( 'legacy-jetpack' === $order ) {
458+
add_action(
459+
'admin_menu',
460+
function () {
461+
remove_action( 'admin_menu', array( 'Akismet_Admin', 'admin_menu' ), 5 );
462+
},
463+
4
464+
);
465+
}
466+
467+
Admin_Menu::add_menu( 'First', 'First', 'edit_posts', 'akismet-first', '__return_null', Admin_Menu::POSITION_FIRST );
468+
469+
do_action( 'admin_menu' );
470+
471+
$slugs = array_column( $submenu['jetpack'], 2 );
472+
473+
$this->assertSame( 'akismet-first', $slugs[0] );
474+
$this->assertCount( 1, array_keys( $slugs, 'akismet-key-config', true ) );
475+
}
476+
477+
/**
478+
* Data provider for test_akismet_item_sorts_below_the_first_item.
479+
*
480+
* @return array
481+
*/
482+
public static function akismet_order_data() {
483+
return array(
484+
'Admin_Menu starts first' => array( 'admin-menu-first' ),
485+
'Akismet hooks in first' => array( 'akismet-first' ),
486+
'an older Jetpack unhooks Akismet itself' => array( 'legacy-jetpack' ),
487+
);
488+
}
489+
409490
/**
410491
* Upgrade item appears in the submenu for an administrator on a free plan.
411492
*

‎projects/packages/admin-ui/tests/php/bootstrap.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,10 @@
2020

2121
define( 'WP_DEBUG', true );
2222

23+
// Process-isolated tests start without one; see the activity-log package's bootstrap for why it matters.
24+
if ( empty( $_SERVER['SCRIPT_FILENAME'] ) ) {
25+
$_SERVER['SCRIPT_FILENAME'] = __FILE__;
26+
}
27+
2328
// Initialize WordPress test environment
2429
\Automattic\Jetpack\Test_Environment::init();
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
/**
3+
* Stand-in for Akismet's admin class.
4+
*
5+
* @package automattic/jetpack-admin-ui
6+
*/
7+
8+
/**
9+
* Mirrors how Akismet registers its menu item when Jetpack is active.
10+
*/
11+
class Akismet_Admin {
12+
/** Hooked on admin_menu at priority 5. */
13+
public static function admin_menu() {
14+
add_action( 'jetpack_admin_menu', array( __CLASS__, 'load_menu' ) );
15+
}
16+
17+
/** Registers the menu item outside Admin_Menu. */
18+
public static function load_menu() {
19+
add_submenu_page( 'jetpack', 'Akismet Anti-spam', 'Akismet Anti-spam', 'manage_options', 'akismet-key-config', array( __CLASS__, 'display_page' ) );
20+
}
21+
22+
/** Page callback. */
23+
public static function display_page() {}
24+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: bugfix
3+
4+
Admin menu: Make the top-level Jetpack menu link open My Jetpack instead of Akismet Anti-spam.

‎projects/plugins/jetpack/class.jetpack-admin.php‎

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,8 @@ private function __construct() {
8888
add_action( 'jetpack_unrecognized_action', array( $this, 'handle_unrecognized_action' ) );
8989

9090
if ( class_exists( 'Akismet_Admin' ) ) {
91-
// If the site has Jetpack Anti-spam, change the Akismet menu label and logo accordingly.
92-
$site_products = array_column( Jetpack_Plan::get_products(), 'product_slug' );
93-
$has_anti_spam_product = count( array_intersect( array( 'jetpack_anti_spam', 'jetpack_anti_spam_monthly' ), $site_products ) ) > 0;
94-
95-
if ( Jetpack_Plan::supports( 'akismet' ) || Jetpack_Plan::supports( 'antispam' ) || $has_anti_spam_product ) {
96-
// Prevent Akismet from adding a menu item.
97-
add_action(
98-
'admin_menu',
99-
function () {
100-
remove_action( 'admin_menu', array( 'Akismet_Admin', 'admin_menu' ), 5 );
101-
},
102-
4
103-
);
104-
105-
// Add an Anti-spam menu item for Jetpack. This is handled automatically by the Admin_Menu as long as it has been initialized.
91+
// Network Admin never fires admin_menu, so starting Admin_Menu there would only add its upgrade item.
92+
if ( ! is_network_admin() ) {
10693
Admin_Menu::init();
10794
}
10895

0 commit comments

Comments
 (0)