From 13ec380fe436a3a7de18d085aa091e625ca1c63a Mon Sep 17 00:00:00 2001 From: nirajgiriXD Date: Tue, 22 Oct 2024 18:32:07 +0530 Subject: [PATCH 01/34] fix: misleading message when accessing admin menu page --- src/wp-admin/includes/menu.php | 13 +++++++++++- src/wp-admin/includes/plugin.php | 34 ++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/includes/menu.php b/src/wp-admin/includes/menu.php index 3cf4a5fdd6c17..cef910c9712fc 100644 --- a/src/wp-admin/includes/menu.php +++ b/src/wp-admin/includes/menu.php @@ -368,7 +368,18 @@ function sort_menu( $a, $b ) { } unset( $last_menu_key ); -if ( ! user_can_access_admin_page() ) { + +if ( ! admin_page_exists() ) { + + /** + * Fires when access to an admin page which does not exist. + * + * @since 4.9.0 + */ + do_action( 'admin_page_not_found' ); + + wp_die( __( 'The requested page does not exist.' ), 403 ); +} elseif ( ! user_can_access_admin_page() ) { /** * Fires when access to an admin page is denied. diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index de1468352b3d9..84fd89965ce65 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2130,6 +2130,31 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { } } +/** + * @since 6.7.0 + * + * @global string $admin_page_parent + * @global string $plugin_page + * @global array $_registered_pages + */ +function admin_page_exists() { + global $admin_page_parent, $plugin_page, $_registered_pages; + + if ( ! isset( $admin_page_parent ) ) { + $admin_page_parent = get_admin_page_parent(); + } + + if ( isset( $plugin_page ) ) { + $hookname = get_plugin_page_hookname( $plugin_page, $admin_page_parent ); + + if ( ! isset( $_registered_pages[$hookname] ) ) { + return false; + } + } + + return true; +} + /** * Gets the hook name for the administrative page of a plugin. * @@ -2175,14 +2200,15 @@ function get_plugin_page_hookname( $plugin_page, $parent_page ) { * @global array $_wp_submenu_nopriv * @global string $plugin_page * @global array $_registered_pages + * @global string $admin_page_parent * * @return bool True if the current user can access the admin page, false otherwise. */ function user_can_access_admin_page() { global $pagenow, $menu, $submenu, $_wp_menu_nopriv, $_wp_submenu_nopriv, - $plugin_page, $_registered_pages; + $plugin_page, $_registered_pages, $admin_page_parent; - $parent = get_admin_page_parent(); + $parent = isset( $admin_page_parent ) ? $admin_page_parent : get_admin_page_parent(); if ( ! isset( $plugin_page ) && isset( $_wp_submenu_nopriv[ $parent ][ $pagenow ] ) ) { return false; @@ -2195,6 +2221,10 @@ function user_can_access_admin_page() { $hookname = get_plugin_page_hookname( $plugin_page, $parent ); + if ( ! admin_page_exists() ) { + return false; + } + if ( ! isset( $_registered_pages[ $hookname ] ) ) { return false; } From c1690410e2f4e5b50d1ac851e5d5f7e038d4fbc5 Mon Sep 17 00:00:00 2001 From: nirajgiriXD Date: Tue, 22 Oct 2024 19:04:09 +0530 Subject: [PATCH 02/34] fix phpcs: remove empty line --- src/wp-admin/includes/menu.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-admin/includes/menu.php b/src/wp-admin/includes/menu.php index cef910c9712fc..96a01b5f93845 100644 --- a/src/wp-admin/includes/menu.php +++ b/src/wp-admin/includes/menu.php @@ -368,7 +368,6 @@ function sort_menu( $a, $b ) { } unset( $last_menu_key ); - if ( ! admin_page_exists() ) { /** From b55ed491ca6248109f7dfafe25c6ae21f0303fee Mon Sep 17 00:00:00 2001 From: nirajgiriXD Date: Tue, 22 Oct 2024 19:08:51 +0530 Subject: [PATCH 03/34] fix phpcs: add space --- src/wp-admin/includes/plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 84fd89965ce65..61a6feaedf7c7 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2147,7 +2147,7 @@ function admin_page_exists() { if ( isset( $plugin_page ) ) { $hookname = get_plugin_page_hookname( $plugin_page, $admin_page_parent ); - if ( ! isset( $_registered_pages[$hookname] ) ) { + if ( ! isset( $_registered_pages[ $hookname ] ) ) { return false; } } From 061fc207fd3ef01ea86fe48332e3dfa34a2c35a6 Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:20:58 +0530 Subject: [PATCH 04/34] Update comment Co-authored-by: Christoph Daum --- src/wp-admin/includes/plugin.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 61a6feaedf7c7..e2f5b38fd2376 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2131,7 +2131,9 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { } /** - * @since 6.7.0 + * Checks if admin page exists, if the user would have the required capabilities. + * + * @since tba * * @global string $admin_page_parent * @global string $plugin_page From 261dbd4a9741e6001eb4a150bae2a7277798d894 Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:08:42 +0545 Subject: [PATCH 05/34] Update src/wp-admin/includes/menu.php Co-authored-by: John Parris --- src/wp-admin/includes/menu.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/menu.php b/src/wp-admin/includes/menu.php index 96a01b5f93845..83e559050a2e5 100644 --- a/src/wp-admin/includes/menu.php +++ b/src/wp-admin/includes/menu.php @@ -373,7 +373,7 @@ function sort_menu( $a, $b ) { /** * Fires when access to an admin page which does not exist. * - * @since 4.9.0 + * @since 6.9.0 */ do_action( 'admin_page_not_found' ); From dd3aca58454a3d6fdf5d546cc18104df4e2b5e93 Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:09:42 +0545 Subject: [PATCH 06/34] Update src/wp-admin/includes/menu.php Co-authored-by: John Parris --- src/wp-admin/includes/menu.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/menu.php b/src/wp-admin/includes/menu.php index 83e559050a2e5..3a75ff9405886 100644 --- a/src/wp-admin/includes/menu.php +++ b/src/wp-admin/includes/menu.php @@ -377,7 +377,7 @@ function sort_menu( $a, $b ) { */ do_action( 'admin_page_not_found' ); - wp_die( __( 'The requested page does not exist.' ), 403 ); + wp_die( __( 'The requested page does not exist.' ), 404 ); } elseif ( ! user_can_access_admin_page() ) { /** From c3c6ba58f2e5e4769254723f469aef115248a179 Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:04:26 +0545 Subject: [PATCH 07/34] Update src/wp-admin/includes/menu.php Co-authored-by: Noruzzaman --- src/wp-admin/includes/menu.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/menu.php b/src/wp-admin/includes/menu.php index 3a75ff9405886..9e7f84250f2f6 100644 --- a/src/wp-admin/includes/menu.php +++ b/src/wp-admin/includes/menu.php @@ -373,7 +373,7 @@ function sort_menu( $a, $b ) { /** * Fires when access to an admin page which does not exist. * - * @since 6.9.0 + * @since 7.0 */ do_action( 'admin_page_not_found' ); From d03c40edff13de77acd3dbb0954b321bcb96b76d Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:04:37 +0545 Subject: [PATCH 08/34] Update src/wp-admin/includes/plugin.php Co-authored-by: Noruzzaman --- src/wp-admin/includes/plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index e2f5b38fd2376..ce168eb4dc1ec 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2135,7 +2135,7 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * * @since tba * - * @global string $admin_page_parent + * @global string $admin_page_parent The parent slug of the current admin page. * @global string $plugin_page * @global array $_registered_pages */ From 9a7a02f7ec3895965082fdf91cf3cdfc50f22948 Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:05:04 +0545 Subject: [PATCH 09/34] Update src/wp-admin/includes/plugin.php Co-authored-by: Noruzzaman --- src/wp-admin/includes/plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index ce168eb4dc1ec..2b2bceea29e8a 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2136,7 +2136,7 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * @since tba * * @global string $admin_page_parent The parent slug of the current admin page. - * @global string $plugin_page + * @global string $plugin_page The plugin page slug being loaded. * @global array $_registered_pages */ function admin_page_exists() { From 24606e852e1bd41441b87ded0a3445475c91696f Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:05:13 +0545 Subject: [PATCH 10/34] Update src/wp-admin/includes/plugin.php Co-authored-by: Noruzzaman --- src/wp-admin/includes/plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 2b2bceea29e8a..9bee340e8152e 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2137,7 +2137,7 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * * @global string $admin_page_parent The parent slug of the current admin page. * @global string $plugin_page The plugin page slug being loaded. - * @global array $_registered_pages + * @global array $_registered_pages Array of all registered admin page hooks. */ function admin_page_exists() { global $admin_page_parent, $plugin_page, $_registered_pages; From 23fd2b8311f7c89078c2e58aac2ba8495dfe7599 Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:05:37 +0545 Subject: [PATCH 11/34] Update src/wp-admin/includes/plugin.php Co-authored-by: Weston Ruter --- src/wp-admin/includes/plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 9bee340e8152e..4f7bc18976b9b 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2133,7 +2133,7 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { /** * Checks if admin page exists, if the user would have the required capabilities. * - * @since tba + * @since 7.0.0 * * @global string $admin_page_parent The parent slug of the current admin page. * @global string $plugin_page The plugin page slug being loaded. From 2f2a4a43414c055d33328f0867ed73fc9ccb7633 Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:06:33 +0545 Subject: [PATCH 12/34] Update src/wp-admin/includes/plugin.php Co-authored-by: Noruzzaman --- src/wp-admin/includes/plugin.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 4f7bc18976b9b..8e3b7505c2821 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2138,6 +2138,8 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * @global string $admin_page_parent The parent slug of the current admin page. * @global string $plugin_page The plugin page slug being loaded. * @global array $_registered_pages Array of all registered admin page hooks. + * + * @return bool True if the admin page exists, false otherwise. */ function admin_page_exists() { global $admin_page_parent, $plugin_page, $_registered_pages; From a95d7399db95da1e16af4fb0b2477bb995a7fe51 Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Tue, 17 Feb 2026 08:48:53 +0545 Subject: [PATCH 13/34] Update src/wp-admin/includes/menu.php Co-authored-by: Shail Mehta --- src/wp-admin/includes/menu.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/menu.php b/src/wp-admin/includes/menu.php index 9e7f84250f2f6..13aa56df6ead4 100644 --- a/src/wp-admin/includes/menu.php +++ b/src/wp-admin/includes/menu.php @@ -373,7 +373,7 @@ function sort_menu( $a, $b ) { /** * Fires when access to an admin page which does not exist. * - * @since 7.0 + * @since 7.0.0 */ do_action( 'admin_page_not_found' ); From ad8e1dfaef88e4ccb45ba025c613056a4a22a630 Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Tue, 17 Feb 2026 08:49:13 +0545 Subject: [PATCH 14/34] Update src/wp-admin/includes/plugin.php Co-authored-by: Huzaifa Al Mesbah --- src/wp-admin/includes/plugin.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 8e3b7505c2821..ab14646fe2d56 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2135,9 +2135,9 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * * @since 7.0.0 * - * @global string $admin_page_parent The parent slug of the current admin page. - * @global string $plugin_page The plugin page slug being loaded. - * @global array $_registered_pages Array of all registered admin page hooks. + * @global string $admin_page_parent The parent slug of the current admin page. + * @global string $plugin_page The plugin page slug being loaded. + * @global array $_registered_pages Array of all registered admin page hooks. * * @return bool True if the admin page exists, false otherwise. */ From e5e363def0f0e7eb74982f7e69bafd2728c1d89a Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Tue, 17 Feb 2026 08:50:32 +0545 Subject: [PATCH 15/34] Update src/wp-admin/includes/plugin.php Co-authored-by: Weston Ruter --- src/wp-admin/includes/plugin.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index ab14646fe2d56..6e19b6ef9cc8d 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2144,9 +2144,7 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { function admin_page_exists() { global $admin_page_parent, $plugin_page, $_registered_pages; - if ( ! isset( $admin_page_parent ) ) { - $admin_page_parent = get_admin_page_parent(); - } + $admin_page_parent ??= get_admin_page_parent(); if ( isset( $plugin_page ) ) { $hookname = get_plugin_page_hookname( $plugin_page, $admin_page_parent ); From c71ac8dd4b2a1cdbc5feb3dbe89a2eb61dc02b09 Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Tue, 17 Feb 2026 09:07:49 +0545 Subject: [PATCH 16/34] Update src/wp-admin/includes/plugin.php Co-authored-by: Weston Ruter --- src/wp-admin/includes/plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 6e19b6ef9cc8d..2bbf0e1d01b57 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2210,7 +2210,7 @@ function user_can_access_admin_page() { global $pagenow, $menu, $submenu, $_wp_menu_nopriv, $_wp_submenu_nopriv, $plugin_page, $_registered_pages, $admin_page_parent; - $parent = isset( $admin_page_parent ) ? $admin_page_parent : get_admin_page_parent(); + $parent = $admin_page_parent ?? get_admin_page_parent(); if ( ! isset( $plugin_page ) && isset( $_wp_submenu_nopriv[ $parent ][ $pagenow ] ) ) { return false; From 0697636777638e377603d59689f3f76938473e8d Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:42:12 +0530 Subject: [PATCH 17/34] Update src/wp-admin/includes/menu.php Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> --- src/wp-admin/includes/menu.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/menu.php b/src/wp-admin/includes/menu.php index dfb3976795ed2..a5bb5668cf514 100644 --- a/src/wp-admin/includes/menu.php +++ b/src/wp-admin/includes/menu.php @@ -377,7 +377,7 @@ function sort_menu( $a, $b ) { /** * Fires when access to an admin page which does not exist. * - * @since 7.0.0 + * @since 7.2.0 */ do_action( 'admin_page_not_found' ); From e7244f79dc5dbcdc1ef1852557b9be476a3cc1d6 Mon Sep 17 00:00:00 2001 From: Niraj Giri <51747980+nirajgiriXD@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:42:24 +0530 Subject: [PATCH 18/34] Update src/wp-admin/includes/plugin.php Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> --- src/wp-admin/includes/plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index b680c2836d704..ab7bf8bfd104b 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2128,7 +2128,7 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { /** * Checks if admin page exists, if the user would have the required capabilities. * - * @since 7.0.0 + * @since 7.2.0 * * @global string $admin_page_parent The parent slug of the current admin page. * @global string $plugin_page The plugin page slug being loaded. From b5f966385f789c525bab592befbb70be8fee461a Mon Sep 17 00:00:00 2001 From: nirajgirixd Date: Tue, 28 Jul 2026 20:33:17 +0545 Subject: [PATCH 19/34] fix: rename admin_page_exists() to wp_admin_page_exists() and fix docblocks --- src/wp-admin/includes/menu.php | 4 ++-- src/wp-admin/includes/plugin.php | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/wp-admin/includes/menu.php b/src/wp-admin/includes/menu.php index 1ebdc3c3a0052..e9f295e2bd2ad 100644 --- a/src/wp-admin/includes/menu.php +++ b/src/wp-admin/includes/menu.php @@ -365,10 +365,10 @@ function sort_menu( $a, $b ) { } unset( $last_menu_key ); -if ( ! admin_page_exists() ) { +if ( ! wp_admin_page_exists() ) { /** - * Fires when access to an admin page which does not exist. + * Fires when a request is made to access an admin page that does not exist. * * @since 7.0.0 */ diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index f5aeaca12d69e..02c2399117ebe 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2125,7 +2125,10 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { } /** - * Checks if admin page exists, if the user would have the required capabilities. + * Determines whether the requested admin page is registered. + * + * This does not check whether the current user has the capability to access + * the page, only whether the page itself exists. * * @since 7.0.0 * @@ -2135,7 +2138,7 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * * @return bool True if the admin page exists, false otherwise. */ -function admin_page_exists() { +function wp_admin_page_exists() { global $admin_page_parent, $plugin_page, $_registered_pages; $admin_page_parent ??= get_admin_page_parent(); @@ -2217,10 +2220,6 @@ function user_can_access_admin_page() { $hookname = get_plugin_page_hookname( $plugin_page, $parent ); - if ( ! admin_page_exists() ) { - return false; - } - if ( ! isset( $_registered_pages[ $hookname ] ) ) { return false; } From 89b1fd1f77968ea51ae0c41789cf67fa310d7fe7 Mon Sep 17 00:00:00 2001 From: nirajgirixd Date: Tue, 28 Jul 2026 20:37:40 +0545 Subject: [PATCH 20/34] fix: add PHPUnit tests for wp_admin_page_exists() --- tests/phpunit/tests/admin/includesPlugin.php | 45 ++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/phpunit/tests/admin/includesPlugin.php b/tests/phpunit/tests/admin/includesPlugin.php index 4d47fb7db0a81..0ddc843edceb4 100644 --- a/tests/phpunit/tests/admin/includesPlugin.php +++ b/tests/phpunit/tests/admin/includesPlugin.php @@ -21,6 +21,12 @@ public static function wpTearDownAfterClass() { self::_restore_mu_plugins(); } + public function tear_down() { + unset( $GLOBALS['plugin_page'], $GLOBALS['admin_page_parent'], $GLOBALS['_registered_pages'] ); + + parent::tear_down(); + } + public function test_get_plugin_data() { $data = get_plugin_data( DIR_TESTDATA . '/plugins/hello.php' ); @@ -74,6 +80,45 @@ public function test_menu_page_url() { wp_set_current_user( $current_user ); } + /** + * @covers ::wp_admin_page_exists + */ + public function test_wp_admin_page_exists_returns_true_when_no_plugin_page_is_set() { + global $plugin_page; + + unset( $plugin_page ); + + $this->assertTrue( wp_admin_page_exists() ); + } + + /** + * @covers ::wp_admin_page_exists + */ + public function test_wp_admin_page_exists_returns_true_for_a_registered_page() { + global $admin_page_parent, $plugin_page, $_registered_pages; + + $admin_page_parent = 'options-general.php'; + $plugin_page = 'testsettings'; + $_registered_pages = array( + get_plugin_page_hookname( $plugin_page, $admin_page_parent ) => true, + ); + + $this->assertTrue( wp_admin_page_exists() ); + } + + /** + * @covers ::wp_admin_page_exists + */ + public function test_wp_admin_page_exists_returns_false_for_an_unregistered_page() { + global $admin_page_parent, $plugin_page, $_registered_pages; + + $admin_page_parent = 'options-general.php'; + $plugin_page = 'does-not-exist'; + $_registered_pages = array(); + + $this->assertFalse( wp_admin_page_exists() ); + } + /** * Tests the position parameter. * From 26c620e9c39c7a1bf2f4dfd5739df626d2321d8c Mon Sep 17 00:00:00 2001 From: nirajgirixd Date: Thu, 17 Sep 2026 09:56:26 +0545 Subject: [PATCH 21/34] fix: treat capability-restricted admin pages as existing in wp_admin_page_exists() --- src/wp-admin/includes/plugin.php | 34 +++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index e87821903bcde..23743cabd0de8 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2129,30 +2129,50 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * Determines whether the requested admin page is registered. * * This does not check whether the current user has the capability to access - * the page, only whether the page itself exists. + * the page, only whether the page itself exists. Pages registered for a + * capability the current user lacks are recorded in the no-privilege + * registries rather than in $_registered_pages, and are still considered + * to exist so that the caller can report a permission error instead of a + * "page not found" error. * * @since 7.2.0 * * @global string $admin_page_parent The parent slug of the current admin page. * @global string $plugin_page The plugin page slug being loaded. * @global array $_registered_pages Array of all registered admin page hooks. + * @global array $_wp_menu_nopriv Array of top-level menu slugs the current user cannot access. + * @global array $_wp_submenu_nopriv Array of submenu slugs the current user cannot access, keyed by parent slug. * * @return bool True if the admin page exists, false otherwise. */ function wp_admin_page_exists() { - global $admin_page_parent, $plugin_page, $_registered_pages; + global $admin_page_parent, $plugin_page, $_registered_pages, + $_wp_menu_nopriv, $_wp_submenu_nopriv; $admin_page_parent ??= get_admin_page_parent(); - if ( isset( $plugin_page ) ) { - $hookname = get_plugin_page_hookname( $plugin_page, $admin_page_parent ); + if ( ! isset( $plugin_page ) ) { + return true; + } - if ( ! isset( $_registered_pages[ $hookname ] ) ) { - return false; + $hookname = get_plugin_page_hookname( $plugin_page, $admin_page_parent ); + + if ( isset( $_registered_pages[ $hookname ] ) ) { + return true; + } + + // The page may be registered for a capability the current user lacks. + if ( isset( $_wp_menu_nopriv[ $plugin_page ] ) ) { + return true; + } + + foreach ( (array) $_wp_submenu_nopriv as $nopriv_submenus ) { + if ( isset( $nopriv_submenus[ $plugin_page ] ) ) { + return true; } } - return true; + return false; } /** From afd6920b72b608b3e1ad09dea436053fff18e8fe Mon Sep 17 00:00:00 2001 From: nirajgirixd Date: Thu, 17 Sep 2026 09:57:11 +0545 Subject: [PATCH 22/34] test: remove duplicate tear_down() and cover capability-restricted admin pages --- tests/phpunit/tests/admin/includesPlugin.php | 49 +++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/tests/phpunit/tests/admin/includesPlugin.php b/tests/phpunit/tests/admin/includesPlugin.php index 48d949ea34270..b0acf40834ebc 100644 --- a/tests/phpunit/tests/admin/includesPlugin.php +++ b/tests/phpunit/tests/admin/includesPlugin.php @@ -26,13 +26,18 @@ public function tear_down() { * Resets the global menu registries modified by the menu API tests. */ private function reset_menu_globals() { - global $menu, $submenu, $admin_page_hooks, $_registered_pages, $_parent_pages; + global $menu, $submenu, $admin_page_hooks, $_registered_pages, $_parent_pages, + $_wp_menu_nopriv, $_wp_submenu_nopriv; - $menu = array(); - $submenu = array(); - $admin_page_hooks = array(); - $_registered_pages = array(); - $_parent_pages = array(); + $menu = array(); + $submenu = array(); + $admin_page_hooks = array(); + $_registered_pages = array(); + $_parent_pages = array(); + $_wp_menu_nopriv = array(); + $_wp_submenu_nopriv = array(); + + unset( $GLOBALS['plugin_page'], $GLOBALS['admin_page_parent'] ); } public static function wpSetUpBeforeClass( $factory ) { @@ -44,12 +49,6 @@ public static function wpTearDownAfterClass() { self::_restore_mu_plugins(); } - public function tear_down() { - unset( $GLOBALS['plugin_page'], $GLOBALS['admin_page_parent'], $GLOBALS['_registered_pages'] ); - - parent::tear_down(); - } - public function test_get_plugin_data() { $data = get_plugin_data( DIR_TESTDATA . '/plugins/hello.php' ); @@ -142,6 +141,32 @@ public function test_wp_admin_page_exists_returns_false_for_an_unregistered_page $this->assertFalse( wp_admin_page_exists() ); } + /** + * Pages the current user cannot access are not added to $_registered_pages, + * but they do exist and must not be reported as missing. + * + * @covers ::wp_admin_page_exists + */ + public function test_wp_admin_page_exists_returns_true_for_a_registered_page_the_user_cannot_access() { + global $admin_page_parent, $plugin_page; + + $current_user = get_current_user_id(); + wp_set_current_user( self::$admin_id ); + + add_menu_page( 'Test Toplevel', 'Test Toplevel', 'manage_options', 'mt-top-level-handle', 'mt_toplevel_page' ); + + // Register a submenu page requiring a capability no user has. + wp_set_current_user( 0 ); + add_submenu_page( 'mt-top-level-handle', 'No Priv', 'No Priv', 'do_not_allow', 'mt-no-priv', 'mt_no_priv_page' ); + + $admin_page_parent = 'mt-top-level-handle'; + $plugin_page = 'mt-no-priv'; + + $this->assertTrue( wp_admin_page_exists() ); + + wp_set_current_user( $current_user ); + } + /** * Tests the position parameter. * From 7cabe0cd027d90a94f514e1455c924257b091f42 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 21 Sep 2026 12:05:13 -0700 Subject: [PATCH 23/34] Docs: Type the admin menu no-privilege globals. `$_wp_menu_nopriv` and `$_wp_submenu_nopriv` were documented as bare `array`, which leaves PHPStan resolving their contents as `mixed` and reporting both a missing iterable value type and an inaccessible offset on every nested lookup. Both shapes are determinable from core. `$_wp_menu_nopriv` is written only in `wp-admin/includes/menu.php`, keyed by top-level menu slug with a value of `true`. `$_wp_submenu_nopriv` is written there and in `add_submenu_page()`, keyed by parent slug then by submenu slug, again with a value of `true`. Every read is an `isset()`, so only key presence is ever consumed. Documents them as `array` and `array>` respectively, on the two functions this branch modifies. Docblocks only; no behavior change. Co-Authored-By: Claude Opus 5 --- src/wp-admin/includes/plugin.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 15e18a28888b5..e3e24a2deea44 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2143,11 +2143,11 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * * @since 7.2.0 * - * @global string $admin_page_parent The parent slug of the current admin page. - * @global string $plugin_page The plugin page slug being loaded. - * @global array $_registered_pages Array of all registered admin page hooks. - * @global array $_wp_menu_nopriv Array of top-level menu slugs the current user cannot access. - * @global array $_wp_submenu_nopriv Array of submenu slugs the current user cannot access, keyed by parent slug. + * @global string $admin_page_parent The parent slug of the current admin page. + * @global string $plugin_page The plugin page slug being loaded. + * @global array $_registered_pages Array of all registered admin page hooks. + * @global array $_wp_menu_nopriv Array of top-level menu slugs the current user cannot access. + * @global array> $_wp_submenu_nopriv Array of submenu slugs the current user cannot access, keyed by parent slug. * * @return bool True if the admin page exists, false otherwise. */ @@ -2219,14 +2219,14 @@ function get_plugin_page_hookname( $plugin_page, $parent_page ) { * * @since 1.5.0 * - * @global string $pagenow The filename of the current screen. - * @global array $menu - * @global array $submenu - * @global array $_wp_menu_nopriv - * @global array $_wp_submenu_nopriv - * @global string $plugin_page - * @global array $_registered_pages - * @global string $admin_page_parent + * @global string $pagenow The filename of the current screen. + * @global array $menu + * @global array $submenu + * @global array $_wp_menu_nopriv + * @global array> $_wp_submenu_nopriv + * @global string $plugin_page + * @global array $_registered_pages + * @global string $admin_page_parent * * @return bool True if the current user can access the admin page, false otherwise. */ From 4e5ca9a3b3cb719dbacef76b963afb332d6a7472 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 21 Sep 2026 12:05:37 -0700 Subject: [PATCH 24/34] Add native return type hints --- src/wp-admin/includes/plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index e3e24a2deea44..a34b5b99e7523 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2151,7 +2151,7 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * * @return bool True if the admin page exists, false otherwise. */ -function wp_admin_page_exists() { +function wp_admin_page_exists(): bool { global $admin_page_parent, $plugin_page, $_registered_pages, $_wp_menu_nopriv, $_wp_submenu_nopriv; @@ -2230,7 +2230,7 @@ function get_plugin_page_hookname( $plugin_page, $parent_page ) { * * @return bool True if the current user can access the admin page, false otherwise. */ -function user_can_access_admin_page() { +function user_can_access_admin_page(): bool { global $pagenow, $menu, $submenu, $_wp_menu_nopriv, $_wp_submenu_nopriv, $plugin_page, $_registered_pages, $admin_page_parent; From 8616ec7018bc3babd676821714083c86426c38b2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 21 Sep 2026 12:47:51 -0700 Subject: [PATCH 25/34] Docs: Describe the globals imported by `user_can_access_admin_page()`. Seven of the eight `@global` tags on the function carried a type and a name but no description. Fills them in, reusing the wording already used for the same globals on `wp_admin_page_exists()` so the two functions read consistently. `$menu` and `$submenu` are described for the first time; neither is documented anywhere else in core. Co-Authored-By: Claude Opus 5 --- src/wp-admin/includes/plugin.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index a34b5b99e7523..d41da0497b8eb 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2220,13 +2220,13 @@ function get_plugin_page_hookname( $plugin_page, $parent_page ) { * @since 1.5.0 * * @global string $pagenow The filename of the current screen. - * @global array $menu - * @global array $submenu - * @global array $_wp_menu_nopriv - * @global array> $_wp_submenu_nopriv - * @global string $plugin_page - * @global array $_registered_pages - * @global string $admin_page_parent + * @global array $menu The top-level admin menu items. + * @global array $submenu The admin submenu items, keyed by parent slug. + * @global array $_wp_menu_nopriv Array of top-level menu slugs the current user cannot access. + * @global array> $_wp_submenu_nopriv Array of submenu slugs the current user cannot access, keyed by parent slug. + * @global string $plugin_page The plugin page slug being loaded. + * @global array $_registered_pages Array of all registered admin page hooks. + * @global string $admin_page_parent The parent slug of the current admin page. * * @return bool True if the current user can access the admin page, false otherwise. */ From 5a321a7a0b1d48ebe0ee2022e8d7acea7c12b340 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Wed, 7 Oct 2026 18:38:56 +0900 Subject: [PATCH 26/34] Remove the `$admin_page_parent` global and look up the parent each time. Co-Authored-By: Claude --- src/wp-admin/includes/plugin.php | 13 ++++--------- tests/phpunit/tests/admin/includesPlugin.php | 15 ++++++--------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 26efe62f54442..91d4c6388c075 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -2147,7 +2147,6 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * * @since 7.2.0 * - * @global string $admin_page_parent The parent slug of the current admin page. * @global string $plugin_page The plugin page slug being loaded. * @global array $_registered_pages Array of all registered admin page hooks. * @global array $_wp_menu_nopriv Array of top-level menu slugs the current user cannot access. @@ -2156,16 +2155,13 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * @return bool True if the admin page exists, false otherwise. */ function wp_admin_page_exists(): bool { - global $admin_page_parent, $plugin_page, $_registered_pages, - $_wp_menu_nopriv, $_wp_submenu_nopriv; - - $admin_page_parent ??= get_admin_page_parent(); + global $plugin_page, $_registered_pages, $_wp_menu_nopriv, $_wp_submenu_nopriv; if ( ! isset( $plugin_page ) ) { return true; } - $hookname = get_plugin_page_hookname( $plugin_page, $admin_page_parent ); + $hookname = get_plugin_page_hookname( $plugin_page, get_admin_page_parent() ); if ( isset( $_registered_pages[ $hookname ] ) ) { return true; @@ -2230,15 +2226,14 @@ function get_plugin_page_hookname( $plugin_page, $parent_page ) { * @global array> $_wp_submenu_nopriv Array of submenu slugs the current user cannot access, keyed by parent slug. * @global string $plugin_page The plugin page slug being loaded. * @global array $_registered_pages Array of all registered admin page hooks. - * @global string $admin_page_parent The parent slug of the current admin page. * * @return bool True if the current user can access the admin page, false otherwise. */ function user_can_access_admin_page(): bool { global $pagenow, $menu, $submenu, $_wp_menu_nopriv, $_wp_submenu_nopriv, - $plugin_page, $_registered_pages, $admin_page_parent; + $plugin_page, $_registered_pages; - $parent = $admin_page_parent ?? get_admin_page_parent(); + $parent = get_admin_page_parent(); if ( ! isset( $plugin_page ) && isset( $_wp_submenu_nopriv[ $parent ][ $pagenow ] ) ) { return false; diff --git a/tests/phpunit/tests/admin/includesPlugin.php b/tests/phpunit/tests/admin/includesPlugin.php index b0acf40834ebc..ef06bc9347ec2 100644 --- a/tests/phpunit/tests/admin/includesPlugin.php +++ b/tests/phpunit/tests/admin/includesPlugin.php @@ -37,7 +37,7 @@ private function reset_menu_globals() { $_wp_menu_nopriv = array(); $_wp_submenu_nopriv = array(); - unset( $GLOBALS['plugin_page'], $GLOBALS['admin_page_parent'] ); + unset( $GLOBALS['plugin_page'] ); } public static function wpSetUpBeforeClass( $factory ) { @@ -117,12 +117,11 @@ public function test_wp_admin_page_exists_returns_true_when_no_plugin_page_is_se * @covers ::wp_admin_page_exists */ public function test_wp_admin_page_exists_returns_true_for_a_registered_page() { - global $admin_page_parent, $plugin_page, $_registered_pages; + global $plugin_page, $_registered_pages; - $admin_page_parent = 'options-general.php'; $plugin_page = 'testsettings'; $_registered_pages = array( - get_plugin_page_hookname( $plugin_page, $admin_page_parent ) => true, + get_plugin_page_hookname( $plugin_page, '' ) => true, ); $this->assertTrue( wp_admin_page_exists() ); @@ -132,9 +131,8 @@ public function test_wp_admin_page_exists_returns_true_for_a_registered_page() { * @covers ::wp_admin_page_exists */ public function test_wp_admin_page_exists_returns_false_for_an_unregistered_page() { - global $admin_page_parent, $plugin_page, $_registered_pages; + global $plugin_page, $_registered_pages; - $admin_page_parent = 'options-general.php'; $plugin_page = 'does-not-exist'; $_registered_pages = array(); @@ -148,7 +146,7 @@ public function test_wp_admin_page_exists_returns_false_for_an_unregistered_page * @covers ::wp_admin_page_exists */ public function test_wp_admin_page_exists_returns_true_for_a_registered_page_the_user_cannot_access() { - global $admin_page_parent, $plugin_page; + global $plugin_page; $current_user = get_current_user_id(); wp_set_current_user( self::$admin_id ); @@ -159,8 +157,7 @@ public function test_wp_admin_page_exists_returns_true_for_a_registered_page_the wp_set_current_user( 0 ); add_submenu_page( 'mt-top-level-handle', 'No Priv', 'No Priv', 'do_not_allow', 'mt-no-priv', 'mt_no_priv_page' ); - $admin_page_parent = 'mt-top-level-handle'; - $plugin_page = 'mt-no-priv'; + $plugin_page = 'mt-no-priv'; $this->assertTrue( wp_admin_page_exists() ); From 45e85f02367a6cf8c839b7994691a63b15958346 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Wed, 7 Oct 2026 18:39:45 +0900 Subject: [PATCH 27/34] Keep a persistent registry of pages the current user cannot access. The no-privilege menu registries are reset in menu.php after `_admin_menu` fires, so pages registered earlier were reported as missing. Co-Authored-By: Claude --- src/wp-admin/includes/plugin.php | 10 +++++-- tests/phpunit/tests/admin/includesPlugin.php | 31 +++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 91d4c6388c075..05bdad83e787a 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -1478,6 +1478,7 @@ function add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $call * @global array $menu * @global array $_wp_real_parent_file * @global bool $_wp_submenu_nopriv + * @global array $_wp_nopriv_pages * @global array $_registered_pages * @global array $_parent_pages * @@ -1496,7 +1497,7 @@ function add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $call */ function add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) { global $submenu, $menu, $_wp_real_parent_file, $_wp_submenu_nopriv, - $_registered_pages, $_parent_pages; + $_wp_nopriv_pages, $_registered_pages, $_parent_pages; $menu_slug = plugin_basename( $menu_slug ); $parent_slug = plugin_basename( $parent_slug ); @@ -1507,6 +1508,7 @@ function add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, if ( ! current_user_can( $capability ) ) { $_wp_submenu_nopriv[ $parent_slug ][ $menu_slug ] = true; + $_wp_nopriv_pages[ $menu_slug ] = true; return false; } @@ -2151,11 +2153,13 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * @global array $_registered_pages Array of all registered admin page hooks. * @global array $_wp_menu_nopriv Array of top-level menu slugs the current user cannot access. * @global array> $_wp_submenu_nopriv Array of submenu slugs the current user cannot access, keyed by parent slug. + * @global array $_wp_nopriv_pages Array of page slugs registered for a capability the current user lacks. * * @return bool True if the admin page exists, false otherwise. */ function wp_admin_page_exists(): bool { - global $plugin_page, $_registered_pages, $_wp_menu_nopriv, $_wp_submenu_nopriv; + global $plugin_page, $_registered_pages, $_wp_menu_nopriv, $_wp_submenu_nopriv, + $_wp_nopriv_pages; if ( ! isset( $plugin_page ) ) { return true; @@ -2168,7 +2172,7 @@ function wp_admin_page_exists(): bool { } // The page may be registered for a capability the current user lacks. - if ( isset( $_wp_menu_nopriv[ $plugin_page ] ) ) { + if ( isset( $_wp_menu_nopriv[ $plugin_page ] ) || isset( $_wp_nopriv_pages[ $plugin_page ] ) ) { return true; } diff --git a/tests/phpunit/tests/admin/includesPlugin.php b/tests/phpunit/tests/admin/includesPlugin.php index ef06bc9347ec2..53c998b864700 100644 --- a/tests/phpunit/tests/admin/includesPlugin.php +++ b/tests/phpunit/tests/admin/includesPlugin.php @@ -27,7 +27,7 @@ public function tear_down() { */ private function reset_menu_globals() { global $menu, $submenu, $admin_page_hooks, $_registered_pages, $_parent_pages, - $_wp_menu_nopriv, $_wp_submenu_nopriv; + $_wp_menu_nopriv, $_wp_submenu_nopriv, $_wp_nopriv_pages; $menu = array(); $submenu = array(); @@ -36,6 +36,7 @@ private function reset_menu_globals() { $_parent_pages = array(); $_wp_menu_nopriv = array(); $_wp_submenu_nopriv = array(); + $_wp_nopriv_pages = array(); unset( $GLOBALS['plugin_page'] ); } @@ -164,6 +165,34 @@ public function test_wp_admin_page_exists_returns_true_for_a_registered_page_the wp_set_current_user( $current_user ); } + /** + * The no-privilege menu registries are reset in menu.php after the + * `_admin_menu` hook fires, so pages registered before that must still + * be considered to exist. + * + * @covers ::wp_admin_page_exists + */ + public function test_wp_admin_page_exists_returns_true_for_an_inaccessible_page_after_nopriv_registries_are_reset() { + global $plugin_page, $_wp_menu_nopriv, $_wp_submenu_nopriv; + + $current_user = get_current_user_id(); + wp_set_current_user( 0 ); + + add_submenu_page( 'options-general.php', 'No Priv', 'No Priv', 'do_not_allow', 'mt-no-priv', 'mt_no_priv_page' ); + + // Simulate menu.php resetting the registries. + $_wp_menu_nopriv = array(); + $_wp_submenu_nopriv = array(); + + $plugin_page = 'mt-no-priv'; + + $exists = wp_admin_page_exists(); + + wp_set_current_user( $current_user ); + + $this->assertTrue( $exists ); + } + /** * Tests the position parameter. * From ff771746f3b1ed69f14a2639bf5bb7fbb4f412dc Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Wed, 7 Oct 2026 18:39:56 +0900 Subject: [PATCH 28/34] Unset the `$plugin_page` global itself in the test. Co-Authored-By: Claude --- tests/phpunit/tests/admin/includesPlugin.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/phpunit/tests/admin/includesPlugin.php b/tests/phpunit/tests/admin/includesPlugin.php index 53c998b864700..b870f66df921d 100644 --- a/tests/phpunit/tests/admin/includesPlugin.php +++ b/tests/phpunit/tests/admin/includesPlugin.php @@ -107,9 +107,7 @@ public function test_menu_page_url() { * @covers ::wp_admin_page_exists */ public function test_wp_admin_page_exists_returns_true_when_no_plugin_page_is_set() { - global $plugin_page; - - unset( $plugin_page ); + unset( $GLOBALS['plugin_page'] ); $this->assertTrue( wp_admin_page_exists() ); } From 3da2ec5bd05cffd4dd408ef72a9983de0714e234 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Wed, 7 Oct 2026 18:41:03 +0900 Subject: [PATCH 29/34] Register a real page in the registered page test. Co-Authored-By: Claude --- tests/phpunit/tests/admin/includesPlugin.php | 21 ++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/tests/admin/includesPlugin.php b/tests/phpunit/tests/admin/includesPlugin.php index b870f66df921d..2c18fd7898844 100644 --- a/tests/phpunit/tests/admin/includesPlugin.php +++ b/tests/phpunit/tests/admin/includesPlugin.php @@ -116,14 +116,23 @@ public function test_wp_admin_page_exists_returns_true_when_no_plugin_page_is_se * @covers ::wp_admin_page_exists */ public function test_wp_admin_page_exists_returns_true_for_a_registered_page() { - global $plugin_page, $_registered_pages; + global $pagenow, $plugin_page; - $plugin_page = 'testsettings'; - $_registered_pages = array( - get_plugin_page_hookname( $plugin_page, '' ) => true, - ); + $current_user = get_current_user_id(); + wp_set_current_user( self::$admin_id ); - $this->assertTrue( wp_admin_page_exists() ); + add_options_page( 'Test Settings', 'Test Settings', 'manage_options', 'testsettings', 'mt_settings_page' ); + + $original_pagenow = $pagenow; + $pagenow = 'options-general.php'; + $plugin_page = 'testsettings'; + + $exists = wp_admin_page_exists(); + + $pagenow = $original_pagenow; + wp_set_current_user( $current_user ); + + $this->assertTrue( $exists ); } /** From abd809ff45a9537b774b7052c37ecf4bc41f3d75 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Mon, 12 Oct 2026 16:31:33 +0900 Subject: [PATCH 30/34] Add a `wp_` prefix to the `admin_page_not_found` hook. Co-Authored-By: Claude --- src/wp-admin/includes/menu.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/menu.php b/src/wp-admin/includes/menu.php index 60efe43ed5dad..ff4635fffc8b2 100644 --- a/src/wp-admin/includes/menu.php +++ b/src/wp-admin/includes/menu.php @@ -383,7 +383,7 @@ function sort_menu( $a, $b ) { * * @since 7.2.0 */ - do_action( 'admin_page_not_found' ); + do_action( 'wp_admin_page_not_found' ); wp_die( __( 'The requested page does not exist.' ), 404 ); } elseif ( ! user_can_access_admin_page() ) { From f8c70922f9e873f08172b3240d20a4eb381d6da3 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Mon, 12 Oct 2026 16:31:50 +0900 Subject: [PATCH 31/34] Remove unnecessary current user restoration from the new tests. The test suite resets the current user during tear down. Co-Authored-By: Claude --- tests/phpunit/tests/admin/includesPlugin.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/phpunit/tests/admin/includesPlugin.php b/tests/phpunit/tests/admin/includesPlugin.php index 2c18fd7898844..988d66479f54d 100644 --- a/tests/phpunit/tests/admin/includesPlugin.php +++ b/tests/phpunit/tests/admin/includesPlugin.php @@ -118,7 +118,6 @@ public function test_wp_admin_page_exists_returns_true_when_no_plugin_page_is_se public function test_wp_admin_page_exists_returns_true_for_a_registered_page() { global $pagenow, $plugin_page; - $current_user = get_current_user_id(); wp_set_current_user( self::$admin_id ); add_options_page( 'Test Settings', 'Test Settings', 'manage_options', 'testsettings', 'mt_settings_page' ); @@ -130,7 +129,6 @@ public function test_wp_admin_page_exists_returns_true_for_a_registered_page() { $exists = wp_admin_page_exists(); $pagenow = $original_pagenow; - wp_set_current_user( $current_user ); $this->assertTrue( $exists ); } @@ -156,7 +154,6 @@ public function test_wp_admin_page_exists_returns_false_for_an_unregistered_page public function test_wp_admin_page_exists_returns_true_for_a_registered_page_the_user_cannot_access() { global $plugin_page; - $current_user = get_current_user_id(); wp_set_current_user( self::$admin_id ); add_menu_page( 'Test Toplevel', 'Test Toplevel', 'manage_options', 'mt-top-level-handle', 'mt_toplevel_page' ); @@ -168,8 +165,6 @@ public function test_wp_admin_page_exists_returns_true_for_a_registered_page_the $plugin_page = 'mt-no-priv'; $this->assertTrue( wp_admin_page_exists() ); - - wp_set_current_user( $current_user ); } /** @@ -182,7 +177,6 @@ public function test_wp_admin_page_exists_returns_true_for_a_registered_page_the public function test_wp_admin_page_exists_returns_true_for_an_inaccessible_page_after_nopriv_registries_are_reset() { global $plugin_page, $_wp_menu_nopriv, $_wp_submenu_nopriv; - $current_user = get_current_user_id(); wp_set_current_user( 0 ); add_submenu_page( 'options-general.php', 'No Priv', 'No Priv', 'do_not_allow', 'mt-no-priv', 'mt_no_priv_page' ); @@ -193,11 +187,7 @@ public function test_wp_admin_page_exists_returns_true_for_an_inaccessible_page_ $plugin_page = 'mt-no-priv'; - $exists = wp_admin_page_exists(); - - wp_set_current_user( $current_user ); - - $this->assertTrue( $exists ); + $this->assertTrue( wp_admin_page_exists() ); } /** From 4b081fe1f79f24839c92c478e8afdc74ca13781d Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Mon, 12 Oct 2026 16:31:53 +0900 Subject: [PATCH 32/34] Add `@ticket` annotations to the new tests. Co-Authored-By: Claude --- tests/phpunit/tests/admin/includesPlugin.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/phpunit/tests/admin/includesPlugin.php b/tests/phpunit/tests/admin/includesPlugin.php index 988d66479f54d..813ba3429c204 100644 --- a/tests/phpunit/tests/admin/includesPlugin.php +++ b/tests/phpunit/tests/admin/includesPlugin.php @@ -104,6 +104,8 @@ public function test_menu_page_url() { } /** + * @ticket 14060 + * * @covers ::wp_admin_page_exists */ public function test_wp_admin_page_exists_returns_true_when_no_plugin_page_is_set() { @@ -113,6 +115,8 @@ public function test_wp_admin_page_exists_returns_true_when_no_plugin_page_is_se } /** + * @ticket 14060 + * * @covers ::wp_admin_page_exists */ public function test_wp_admin_page_exists_returns_true_for_a_registered_page() { @@ -134,6 +138,8 @@ public function test_wp_admin_page_exists_returns_true_for_a_registered_page() { } /** + * @ticket 14060 + * * @covers ::wp_admin_page_exists */ public function test_wp_admin_page_exists_returns_false_for_an_unregistered_page() { @@ -149,6 +155,8 @@ public function test_wp_admin_page_exists_returns_false_for_an_unregistered_page * Pages the current user cannot access are not added to $_registered_pages, * but they do exist and must not be reported as missing. * + * @ticket 14060 + * * @covers ::wp_admin_page_exists */ public function test_wp_admin_page_exists_returns_true_for_a_registered_page_the_user_cannot_access() { @@ -172,6 +180,8 @@ public function test_wp_admin_page_exists_returns_true_for_a_registered_page_the * `_admin_menu` hook fires, so pages registered before that must still * be considered to exist. * + * @ticket 14060 + * * @covers ::wp_admin_page_exists */ public function test_wp_admin_page_exists_returns_true_for_an_inaccessible_page_after_nopriv_registries_are_reset() { From 369fb0d11936adab0e8a41e872cb5e2f0942498c Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Tue, 13 Oct 2026 16:55:10 +0900 Subject: [PATCH 33/34] Use richer types for the admin page `@global` annotations. Co-Authored-By: Claude --- src/wp-admin/includes/plugin.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php index 05bdad83e787a..9f1a60104465b 100644 --- a/src/wp-admin/includes/plugin.php +++ b/src/wp-admin/includes/plugin.php @@ -1474,13 +1474,13 @@ function add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $call * @since 1.5.0 * @since 5.3.0 Added the `$position` parameter. * - * @global array $submenu - * @global array $menu - * @global array $_wp_real_parent_file - * @global bool $_wp_submenu_nopriv - * @global array $_wp_nopriv_pages - * @global array $_registered_pages - * @global array $_parent_pages + * @global array $submenu + * @global array $menu + * @global array $_wp_real_parent_file + * @global array> $_wp_submenu_nopriv + * @global array $_wp_nopriv_pages + * @global array $_registered_pages + * @global array $_parent_pages * * @param string $parent_slug The slug name for the parent menu (or the file name of a standard * WordPress admin page). @@ -2149,7 +2149,7 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) { * * @since 7.2.0 * - * @global string $plugin_page The plugin page slug being loaded. + * @global string|null $plugin_page The plugin page slug being loaded. * @global array $_registered_pages Array of all registered admin page hooks. * @global array $_wp_menu_nopriv Array of top-level menu slugs the current user cannot access. * @global array> $_wp_submenu_nopriv Array of submenu slugs the current user cannot access, keyed by parent slug. @@ -2228,8 +2228,8 @@ function get_plugin_page_hookname( $plugin_page, $parent_page ) { * @global array $submenu The admin submenu items, keyed by parent slug. * @global array $_wp_menu_nopriv Array of top-level menu slugs the current user cannot access. * @global array> $_wp_submenu_nopriv Array of submenu slugs the current user cannot access, keyed by parent slug. - * @global string $plugin_page The plugin page slug being loaded. - * @global array $_registered_pages Array of all registered admin page hooks. + * @global string|null $plugin_page The plugin page slug being loaded. + * @global array $_registered_pages Array of all registered admin page hooks. * * @return bool True if the current user can access the admin page, false otherwise. */ From f3b6128f8c19d5dabd9b78bd4b1f6dcf6b155144 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Tue, 13 Oct 2026 17:01:07 +0900 Subject: [PATCH 34/34] Remove the PHPStan baseline entry fixed by the `$_wp_submenu_nopriv` type correction. Co-Authored-By: Claude --- .../phpstan/baselines/offsetAccess.nonOffsetAccessible.neon | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/phpstan/baselines/offsetAccess.nonOffsetAccessible.neon b/tests/phpstan/baselines/offsetAccess.nonOffsetAccessible.neon index 5b2396b941816..9179b5365005f 100644 --- a/tests/phpstan/baselines/offsetAccess.nonOffsetAccessible.neon +++ b/tests/phpstan/baselines/offsetAccess.nonOffsetAccessible.neon @@ -23,11 +23,6 @@ parameters: identifier: offsetAccess.nonOffsetAccessible count: 4 path: ../../../src/wp-admin/includes/class-wp-debug-data.php - - - message: '#^Cannot access offset mixed on bool\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: ../../../src/wp-admin/includes/plugin.php - message: '#^Cannot access offset ''new_version'' on bool\.$#' identifier: offsetAccess.nonOffsetAccessible