From f8bdfa03f3326b5f56f864f0e6060d5b26f8f21e Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Fri, 3 Jul 2026 14:58:03 +0530 Subject: [PATCH 1/2] Admin: Use absolute URLs in admin menu links --- src/wp-admin/menu-header.php | 36 ++++-- tests/phpunit/tests/admin/menuHeader.php | 148 +++++++++++++++++++++++ 2 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 tests/phpunit/tests/admin/menuHeader.php diff --git a/src/wp-admin/menu-header.php b/src/wp-admin/menu-header.php index dcdf58d6391ce..9249e8deea81c 100644 --- a/src/wp-admin/menu-header.php +++ b/src/wp-admin/menu-header.php @@ -54,6 +54,24 @@ get_admin_page_parent(); +/** + * Returns an absolute URL for an admin menu item. + * + * @since 6.9.0 + * @access private + * + * @param string $path The admin menu item path. + * @return string The escaped absolute admin menu item URL. + */ +function _wp_admin_menu_url( $path ) { + + if ( preg_match( '#^[a-z][a-z0-9+.-]*:#i', $path ) || str_starts_with( $path, '//' ) ) { + return esc_url( $path ); + } + + return esc_url( self_admin_url( $path ) ); +} + /** * Display menu. * @@ -170,9 +188,11 @@ function _wp_menu_output( $menu, $submenu, $submenu_as_parent = true ) { && ! file_exists( ABSPATH . "/wp-admin/$menu_file" ) ) ) { $admin_is_parent = true; - echo ""; + $menu_url = _wp_admin_menu_url( add_query_arg( 'page', $submenu_items[0][2], 'admin.php' ) ); + echo ""; } else { - echo "\n\t"; + $menu_url = _wp_admin_menu_url( $submenu_items[0][2] ); + echo "\n\t"; } } elseif ( ! empty( $item[2] ) && current_user_can( $item[1] ) ) { $menu_hook = get_plugin_page_hook( $item[2], 'admin.php' ); @@ -189,12 +209,13 @@ function _wp_menu_output( $menu, $submenu, $submenu_as_parent = true ) { && ! file_exists( ABSPATH . "/wp-admin/$menu_file" ) ) ) { $admin_is_parent = true; - echo "\n\t"; + $menu_url = _wp_admin_menu_url( add_query_arg( 'page', $item[2], 'admin.php' ) ); + echo "\n\t"; } else { - echo "\n\t"; + $menu_url = _wp_admin_menu_url( $item[2] ); + echo "\n\t"; } } - if ( ! empty( $submenu_items ) ) { echo "\n\t'; diff --git a/tests/phpunit/tests/admin/menuHeader.php b/tests/phpunit/tests/admin/menuHeader.php new file mode 100644 index 0000000000000..ae64db3f3d810 --- /dev/null +++ b/tests/phpunit/tests/admin/menuHeader.php @@ -0,0 +1,148 @@ +user->create( array( 'role' => 'administrator' ) ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + } + + /** + * @ticket 56302 + */ + public function test_wp_menu_output_uses_absolute_top_level_url() { + wp_set_current_user( self::$admin_id ); + update_option( 'siteurl', 'http://example.com' ); + + $menu = array( + array( 'Dashboard', 'read', 'index.php', 'Dashboard', 'menu-top', 'menu-dashboard', 'dashicons-dashboard' ), + ); + + $actual = $this->render_admin_menu( $menu, array() ); + + $this->assertStringContainsString( "href='http://example.com/wp-admin/index.php'", $actual ); + } + + /** + * @ticket 56302 + */ + public function test_wp_menu_output_uses_absolute_submenu_parent_url() { + wp_set_current_user( self::$admin_id ); + update_option( 'siteurl', 'http://example.com' ); + + $menu = array( + array( 'Tools', 'read', 'tools.php', 'Tools', 'menu-top', 'menu-tools', 'dashicons-admin-tools' ), + ); + $submenu = array( + 'tools.php' => array( + array( 'Available Tools', 'read', 'tools.php', 'Available Tools' ), + ), + ); + + $actual = $this->render_admin_menu( $menu, $submenu ); + + $this->assertStringContainsString( "href='http://example.com/wp-admin/tools.php'", $actual ); + } + + /** + * @ticket 56302 + */ + public function test_wp_menu_output_uses_absolute_submenu_urls() { + wp_set_current_user( self::$admin_id ); + update_option( 'siteurl', 'http://example.com' ); + + $menu = array( + array( 'Posts', 'read', 'edit.php', 'Posts', 'menu-top', 'menu-posts', 'dashicons-admin-post' ), + ); + $submenu = array( + 'edit.php' => array( + array( 'All Posts', 'read', 'edit.php', 'All Posts' ), + array( 'Add New', 'read', 'post-new.php?post_type=post', 'Add New' ), + ), + ); + + $actual = $this->render_admin_menu( $menu, $submenu ); + + $this->assertStringContainsString( "href='http://example.com/wp-admin/edit.php'", $actual ); + $this->assertStringContainsString( "href='http://example.com/wp-admin/post-new.php?post_type=post'", $actual ); + } + + /** + * @ticket 56302 + */ + public function test_wp_menu_output_uses_absolute_plugin_page_url() { + wp_set_current_user( self::$admin_id ); + update_option( 'siteurl', 'http://example.com' ); + + add_menu_page( 'Test Page', 'Test Page', 'read', 'test-page', '__return_null' ); + + global $menu; + + $actual = $this->render_admin_menu( $menu, array(), false ); + + $this->assertStringContainsString( "href='http://example.com/wp-admin/admin.php?page=test-page'", $actual ); + } + + /** + * @ticket 56302 + */ + public function test_wp_menu_output_preserves_external_urls() { + wp_set_current_user( self::$admin_id ); + update_option( 'siteurl', 'http://example.com' ); + + $menu = array( + array( 'External', 'read', 'https://wordpress.org/news/', 'External', 'menu-top', 'menu-external', 'dashicons-admin-site' ), + ); + + $actual = $this->render_admin_menu( $menu, array() ); + + $this->assertStringContainsString( "href='https://wordpress.org/news/'", $actual ); + } + + private function render_admin_menu( $menu, $submenu, $submenu_as_parent = true ) { + $this->require_menu_header(); + + global $self, $parent_file, $submenu_file, $plugin_page, $typenow; + + $self = 'index.php'; + $parent_file = ''; + $submenu_file = ''; + $plugin_page = null; + $typenow = ''; + + ob_start(); + _wp_menu_output( $menu, $submenu, $submenu_as_parent ); + return ob_get_clean(); + } + + private function require_menu_header() { + if ( function_exists( '_wp_menu_output' ) ) { + return; + } + + global $menu, $submenu, $parent_file, $submenu_file; + + $menu = array(); + $submenu = array(); + $parent_file = ''; + $submenu_file = ''; + + $_SERVER['PHP_SELF'] = '/wp-admin/index.php'; + + ob_start(); + require ABSPATH . 'wp-admin/menu-header.php'; + ob_end_clean(); + } +} From 22138b31f170835da91c0289f1e4c2744c03b632 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Fri, 3 Jul 2026 15:28:12 +0530 Subject: [PATCH 2/2] Tests: Isolate admin menu URL test state --- tests/phpunit/tests/admin/menuHeader.php | 90 +++++++++++++++--------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/tests/phpunit/tests/admin/menuHeader.php b/tests/phpunit/tests/admin/menuHeader.php index ae64db3f3d810..4858fdcab4774 100644 --- a/tests/phpunit/tests/admin/menuHeader.php +++ b/tests/phpunit/tests/admin/menuHeader.php @@ -4,27 +4,11 @@ */ class Tests_Admin_MenuHeader extends WP_UnitTestCase { - /** - * Admin user ID. - * - * @var int - */ - public static $admin_id; - - public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { - self::$admin_id = $factory->user->create( array( 'role' => 'administrator' ) ); - } - - public static function wpTearDownAfterClass() { - self::delete_user( self::$admin_id ); - } - /** * @ticket 56302 */ public function test_wp_menu_output_uses_absolute_top_level_url() { - wp_set_current_user( self::$admin_id ); - update_option( 'siteurl', 'http://example.com' ); + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); $menu = array( array( 'Dashboard', 'read', 'index.php', 'Dashboard', 'menu-top', 'menu-dashboard', 'dashicons-dashboard' ), @@ -32,15 +16,14 @@ public function test_wp_menu_output_uses_absolute_top_level_url() { $actual = $this->render_admin_menu( $menu, array() ); - $this->assertStringContainsString( "href='http://example.com/wp-admin/index.php'", $actual ); + $this->assertStringContainsString( "href='" . admin_url( 'index.php' ) . "'", $actual ); } /** * @ticket 56302 */ public function test_wp_menu_output_uses_absolute_submenu_parent_url() { - wp_set_current_user( self::$admin_id ); - update_option( 'siteurl', 'http://example.com' ); + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); $menu = array( array( 'Tools', 'read', 'tools.php', 'Tools', 'menu-top', 'menu-tools', 'dashicons-admin-tools' ), @@ -53,15 +36,14 @@ public function test_wp_menu_output_uses_absolute_submenu_parent_url() { $actual = $this->render_admin_menu( $menu, $submenu ); - $this->assertStringContainsString( "href='http://example.com/wp-admin/tools.php'", $actual ); + $this->assertStringContainsString( "href='" . admin_url( 'tools.php' ) . "'", $actual ); } /** * @ticket 56302 */ public function test_wp_menu_output_uses_absolute_submenu_urls() { - wp_set_current_user( self::$admin_id ); - update_option( 'siteurl', 'http://example.com' ); + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); $menu = array( array( 'Posts', 'read', 'edit.php', 'Posts', 'menu-top', 'menu-posts', 'dashicons-admin-post' ), @@ -75,16 +57,15 @@ public function test_wp_menu_output_uses_absolute_submenu_urls() { $actual = $this->render_admin_menu( $menu, $submenu ); - $this->assertStringContainsString( "href='http://example.com/wp-admin/edit.php'", $actual ); - $this->assertStringContainsString( "href='http://example.com/wp-admin/post-new.php?post_type=post'", $actual ); + $this->assertStringContainsString( "href='" . admin_url( 'edit.php' ) . "'", $actual ); + $this->assertStringContainsString( "href='" . admin_url( 'post-new.php?post_type=post' ) . "'", $actual ); } /** * @ticket 56302 */ public function test_wp_menu_output_uses_absolute_plugin_page_url() { - wp_set_current_user( self::$admin_id ); - update_option( 'siteurl', 'http://example.com' ); + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); add_menu_page( 'Test Page', 'Test Page', 'read', 'test-page', '__return_null' ); @@ -92,15 +73,14 @@ public function test_wp_menu_output_uses_absolute_plugin_page_url() { $actual = $this->render_admin_menu( $menu, array(), false ); - $this->assertStringContainsString( "href='http://example.com/wp-admin/admin.php?page=test-page'", $actual ); + $this->assertStringContainsString( "href='" . admin_url( 'admin.php?page=test-page' ) . "'", $actual ); } /** * @ticket 56302 */ public function test_wp_menu_output_preserves_external_urls() { - wp_set_current_user( self::$admin_id ); - update_option( 'siteurl', 'http://example.com' ); + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); $menu = array( array( 'External', 'read', 'https://wordpress.org/news/', 'External', 'menu-top', 'menu-external', 'dashicons-admin-site' ), @@ -116,6 +96,8 @@ private function render_admin_menu( $menu, $submenu, $submenu_as_parent = true ) global $self, $parent_file, $submenu_file, $plugin_page, $typenow; + $globals = $this->backup_globals( array( 'self', 'parent_file', 'submenu_file', 'plugin_page', 'typenow' ) ); + $self = 'index.php'; $parent_file = ''; $submenu_file = ''; @@ -123,8 +105,12 @@ private function render_admin_menu( $menu, $submenu, $submenu_as_parent = true ) $typenow = ''; ob_start(); - _wp_menu_output( $menu, $submenu, $submenu_as_parent ); - return ob_get_clean(); + try { + _wp_menu_output( $menu, $submenu, $submenu_as_parent ); + return ob_get_clean(); + } finally { + $this->restore_globals( $globals ); + } } private function require_menu_header() { @@ -134,6 +120,9 @@ private function require_menu_header() { global $menu, $submenu, $parent_file, $submenu_file; + $globals = $this->backup_globals( array( 'menu', 'submenu', 'parent_file', 'submenu_file' ) ); + $php_self = isset( $_SERVER['PHP_SELF'] ) ? $_SERVER['PHP_SELF'] : null; + $menu = array(); $submenu = array(); $parent_file = ''; @@ -142,7 +131,40 @@ private function require_menu_header() { $_SERVER['PHP_SELF'] = '/wp-admin/index.php'; ob_start(); - require ABSPATH . 'wp-admin/menu-header.php'; - ob_end_clean(); + try { + require ABSPATH . 'wp-admin/menu-header.php'; + ob_end_clean(); + } finally { + $this->restore_globals( $globals ); + + if ( null === $php_self ) { + unset( $_SERVER['PHP_SELF'] ); + } else { + $_SERVER['PHP_SELF'] = $php_self; + } + } + } + + private function backup_globals( $names ) { + $globals = array(); + + foreach ( $names as $name ) { + $globals[ $name ] = array( + 'exists' => array_key_exists( $name, $GLOBALS ), + 'value' => array_key_exists( $name, $GLOBALS ) ? $GLOBALS[ $name ] : null, + ); + } + + return $globals; + } + + private function restore_globals( $globals ) { + foreach ( $globals as $name => $global ) { + if ( $global['exists'] ) { + $GLOBALS[ $name ] = $global['value']; + } else { + unset( $GLOBALS[ $name ] ); + } + } } }