Skip to content

Commit 6dc9323

Browse files
committed
Administration: Display 404 error for unregistered admin pages.
Replaces the misleading "sorry, you are not allowed to access this page" message with "the requested page does not exist" when a user attempts to visit an unregistered page in the dashboard. Unregistered pages now return an HTTP 404 response rather than a 403. Props ahsannayem, apermo, arifulislam1, audrasjb, chinmoy709, chriscct7, dd32, envygeeks, habiburdev, hakre, huzaifaalmesbah, jane, jorbin, marzanhusain, mindctrl, muryam, nacin, nikschavan, nirajgirixd, noruzzaman, ornani, ozgursar, pento, poojapadamad, presskopp, ramiy, sainathpoojary, sergeybiryukov, shailu25, sirlouen, syedjakir91, thabotswana, vgnavada, westonruter, wildworks Fixes #14060. git-svn-id: https://develop.svn.wordpress.org/trunk@64351 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 1af116d commit 6dc9323

4 files changed

Lines changed: 185 additions & 27 deletions

File tree

‎src/wp-admin/includes/menu.php‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,17 @@ function sort_menu( $a, $b ) {
376376
}
377377
unset( $last_menu_key );
378378

379-
if ( ! user_can_access_admin_page() ) {
379+
if ( ! wp_admin_page_exists() ) {
380+
381+
/**
382+
* Fires when a request is made to access an admin page that does not exist.
383+
*
384+
* @since 7.2.0
385+
*/
386+
do_action( 'wp_admin_page_not_found' );
387+
388+
wp_die( __( 'The requested page does not exist.' ), 404 );
389+
} elseif ( ! user_can_access_admin_page() ) {
380390

381391
/**
382392
* Fires when access to an admin page is denied.

‎src/wp-admin/includes/plugin.php‎

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1477,12 +1477,13 @@ function add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $call
14771477
* @since 1.5.0
14781478
* @since 5.3.0 Added the `$position` parameter.
14791479
*
1480-
* @global array $submenu
1481-
* @global array $menu
1482-
* @global array $_wp_real_parent_file
1483-
* @global bool $_wp_submenu_nopriv
1484-
* @global array $_registered_pages
1485-
* @global array $_parent_pages
1480+
* @global array $submenu
1481+
* @global array $menu
1482+
* @global array $_wp_real_parent_file
1483+
* @global array<string, array<string, true>> $_wp_submenu_nopriv
1484+
* @global array<string, true> $_wp_nopriv_pages
1485+
* @global array<string, true> $_registered_pages
1486+
* @global array $_parent_pages
14861487
*
14871488
* @param string $parent_slug The slug name for the parent menu (or the file name of a standard
14881489
* WordPress admin page).
@@ -1501,7 +1502,7 @@ function add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $call
15011502
*/
15021503
function add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, $menu_slug, $callback = '', $position = null ) {
15031504
global $submenu, $menu, $_wp_real_parent_file, $_wp_submenu_nopriv,
1504-
$_registered_pages, $_parent_pages;
1505+
$_wp_nopriv_pages, $_registered_pages, $_parent_pages;
15051506

15061507
$menu_slug = plugin_basename( $menu_slug );
15071508
$parent_slug = plugin_basename( $parent_slug );
@@ -1512,6 +1513,7 @@ function add_submenu_page( $parent_slug, $page_title, $menu_title, $capability,
15121513

15131514
if ( ! current_user_can( $capability ) ) {
15141515
$_wp_submenu_nopriv[ $parent_slug ][ $menu_slug ] = true;
1516+
$_wp_nopriv_pages[ $menu_slug ] = true;
15151517
return false;
15161518
}
15171519

@@ -2162,6 +2164,54 @@ function get_plugin_page_hook( $plugin_page, $parent_page ) {
21622164
}
21632165
}
21642166

2167+
/**
2168+
* Determines whether the requested admin page is registered.
2169+
*
2170+
* This does not check whether the current user has the capability to access
2171+
* the page, only whether the page itself exists. Pages registered for a
2172+
* capability the current user lacks are recorded in the no-privilege
2173+
* registries rather than in $_registered_pages, and are still considered
2174+
* to exist so that the caller can report a permission error instead of a
2175+
* "page not found" error.
2176+
*
2177+
* @since 7.2.0
2178+
*
2179+
* @global string|null $plugin_page The plugin page slug being loaded.
2180+
* @global array<string, true> $_registered_pages Array of all registered admin page hooks.
2181+
* @global array<string, true> $_wp_menu_nopriv Array of top-level menu slugs the current user cannot access.
2182+
* @global array<string, array<string, true>> $_wp_submenu_nopriv Array of submenu slugs the current user cannot access, keyed by parent slug.
2183+
* @global array<string, true> $_wp_nopriv_pages Array of page slugs registered for a capability the current user lacks.
2184+
*
2185+
* @return bool True if the admin page exists, false otherwise.
2186+
*/
2187+
function wp_admin_page_exists(): bool {
2188+
global $plugin_page, $_registered_pages, $_wp_menu_nopriv, $_wp_submenu_nopriv,
2189+
$_wp_nopriv_pages;
2190+
2191+
if ( ! isset( $plugin_page ) ) {
2192+
return true;
2193+
}
2194+
2195+
$hookname = get_plugin_page_hookname( $plugin_page, get_admin_page_parent() );
2196+
2197+
if ( isset( $_registered_pages[ $hookname ] ) ) {
2198+
return true;
2199+
}
2200+
2201+
// The page may be registered for a capability the current user lacks.
2202+
if ( isset( $_wp_menu_nopriv[ $plugin_page ] ) || isset( $_wp_nopriv_pages[ $plugin_page ] ) ) {
2203+
return true;
2204+
}
2205+
2206+
foreach ( (array) $_wp_submenu_nopriv as $nopriv_submenus ) {
2207+
if ( isset( $nopriv_submenus[ $plugin_page ] ) ) {
2208+
return true;
2209+
}
2210+
}
2211+
2212+
return false;
2213+
}
2214+
21652215
/**
21662216
* Gets the hook name for the administrative page of a plugin.
21672217
*
@@ -2202,17 +2252,17 @@ function get_plugin_page_hookname( $plugin_page, $parent_page ) {
22022252
*
22032253
* @since 1.5.0
22042254
*
2205-
* @global string $pagenow The filename of the current screen.
2206-
* @global array $menu
2207-
* @global array $submenu
2208-
* @global array $_wp_menu_nopriv
2209-
* @global array $_wp_submenu_nopriv
2210-
* @global string $plugin_page
2211-
* @global array $_registered_pages
2255+
* @global string $pagenow The filename of the current screen.
2256+
* @global array $menu The top-level admin menu items.
2257+
* @global array $submenu The admin submenu items, keyed by parent slug.
2258+
* @global array<string, true> $_wp_menu_nopriv Array of top-level menu slugs the current user cannot access.
2259+
* @global array<string, array<string, true>> $_wp_submenu_nopriv Array of submenu slugs the current user cannot access, keyed by parent slug.
2260+
* @global string|null $plugin_page The plugin page slug being loaded.
2261+
* @global array<string, true> $_registered_pages Array of all registered admin page hooks.
22122262
*
22132263
* @return bool True if the current user can access the admin page, false otherwise.
22142264
*/
2215-
function user_can_access_admin_page() {
2265+
function user_can_access_admin_page(): bool {
22162266
global $pagenow, $menu, $submenu, $_wp_menu_nopriv, $_wp_submenu_nopriv,
22172267
$plugin_page, $_registered_pages;
22182268

‎tests/phpstan/baselines/offsetAccess.nonOffsetAccessible.neon‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ parameters:
2323
identifier: offsetAccess.nonOffsetAccessible
2424
count: 4
2525
path: ../../../src/wp-admin/includes/class-wp-debug-data.php
26-
-
27-
message: '#^Cannot access offset mixed on bool\.$#'
28-
identifier: offsetAccess.nonOffsetAccessible
29-
count: 1
30-
path: ../../../src/wp-admin/includes/plugin.php
3126
-
3227
message: '#^Cannot access offset ''new_version'' on bool\.$#'
3328
identifier: offsetAccess.nonOffsetAccessible

‎tests/phpunit/tests/admin/includesPlugin.php‎

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,19 @@ public function tear_down() {
2626
* Resets the global menu registries modified by the menu API tests.
2727
*/
2828
private function reset_menu_globals() {
29-
global $menu, $submenu, $admin_page_hooks, $_registered_pages, $_parent_pages;
29+
global $menu, $submenu, $admin_page_hooks, $_registered_pages, $_parent_pages,
30+
$_wp_menu_nopriv, $_wp_submenu_nopriv, $_wp_nopriv_pages;
3031

31-
$menu = array();
32-
$submenu = array();
33-
$admin_page_hooks = array();
34-
$_registered_pages = array();
35-
$_parent_pages = array();
32+
$menu = array();
33+
$submenu = array();
34+
$admin_page_hooks = array();
35+
$_registered_pages = array();
36+
$_parent_pages = array();
37+
$_wp_menu_nopriv = array();
38+
$_wp_submenu_nopriv = array();
39+
$_wp_nopriv_pages = array();
40+
41+
unset( $GLOBALS['plugin_page'] );
3642
}
3743

3844
public static function wpSetUpBeforeClass( $factory ) {
@@ -97,6 +103,103 @@ public function test_menu_page_url() {
97103
wp_set_current_user( $current_user );
98104
}
99105

106+
/**
107+
* @ticket 14060
108+
*
109+
* @covers ::wp_admin_page_exists
110+
*/
111+
public function test_wp_admin_page_exists_returns_true_when_no_plugin_page_is_set() {
112+
unset( $GLOBALS['plugin_page'] );
113+
114+
$this->assertTrue( wp_admin_page_exists() );
115+
}
116+
117+
/**
118+
* @ticket 14060
119+
*
120+
* @covers ::wp_admin_page_exists
121+
*/
122+
public function test_wp_admin_page_exists_returns_true_for_a_registered_page() {
123+
global $pagenow, $plugin_page;
124+
125+
wp_set_current_user( self::$admin_id );
126+
127+
add_options_page( 'Test Settings', 'Test Settings', 'manage_options', 'testsettings', 'mt_settings_page' );
128+
129+
$original_pagenow = $pagenow;
130+
$pagenow = 'options-general.php';
131+
$plugin_page = 'testsettings';
132+
133+
$exists = wp_admin_page_exists();
134+
135+
$pagenow = $original_pagenow;
136+
137+
$this->assertTrue( $exists );
138+
}
139+
140+
/**
141+
* @ticket 14060
142+
*
143+
* @covers ::wp_admin_page_exists
144+
*/
145+
public function test_wp_admin_page_exists_returns_false_for_an_unregistered_page() {
146+
global $plugin_page, $_registered_pages;
147+
148+
$plugin_page = 'does-not-exist';
149+
$_registered_pages = array();
150+
151+
$this->assertFalse( wp_admin_page_exists() );
152+
}
153+
154+
/**
155+
* Pages the current user cannot access are not added to $_registered_pages,
156+
* but they do exist and must not be reported as missing.
157+
*
158+
* @ticket 14060
159+
*
160+
* @covers ::wp_admin_page_exists
161+
*/
162+
public function test_wp_admin_page_exists_returns_true_for_a_registered_page_the_user_cannot_access() {
163+
global $plugin_page;
164+
165+
wp_set_current_user( self::$admin_id );
166+
167+
add_menu_page( 'Test Toplevel', 'Test Toplevel', 'manage_options', 'mt-top-level-handle', 'mt_toplevel_page' );
168+
169+
// Register a submenu page requiring a capability no user has.
170+
wp_set_current_user( 0 );
171+
add_submenu_page( 'mt-top-level-handle', 'No Priv', 'No Priv', 'do_not_allow', 'mt-no-priv', 'mt_no_priv_page' );
172+
173+
$plugin_page = 'mt-no-priv';
174+
175+
$this->assertTrue( wp_admin_page_exists() );
176+
}
177+
178+
/**
179+
* The no-privilege menu registries are reset in menu.php after the
180+
* `_admin_menu` hook fires, so pages registered before that must still
181+
* be considered to exist.
182+
*
183+
* @ticket 14060
184+
*
185+
* @covers ::wp_admin_page_exists
186+
*/
187+
public function test_wp_admin_page_exists_returns_true_for_an_inaccessible_page_after_nopriv_registries_are_reset() {
188+
global $plugin_page, $_wp_menu_nopriv, $_wp_submenu_nopriv;
189+
190+
wp_set_current_user( 0 );
191+
192+
add_submenu_page( 'options-general.php', 'No Priv', 'No Priv', 'do_not_allow', 'mt-no-priv', 'mt_no_priv_page' );
193+
194+
// Simulate menu.php resetting the registries.
195+
$_wp_menu_nopriv = array();
196+
$_wp_submenu_nopriv = array();
197+
198+
$plugin_page = 'mt-no-priv';
199+
200+
$this->assertTrue( wp_admin_page_exists() );
201+
}
202+
100203
/**
101204
* Tests the position parameter.
102205
*

0 commit comments

Comments
 (0)