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..4858fdcab4774 --- /dev/null +++ b/tests/phpunit/tests/admin/menuHeader.php @@ -0,0 +1,170 @@ +user->create( array( 'role' => 'administrator' ) ) ); + + $menu = array( + array( 'Dashboard', 'read', 'index.php', 'Dashboard', 'menu-top', 'menu-dashboard', 'dashicons-dashboard' ), + ); + + $actual = $this->render_admin_menu( $menu, array() ); + + $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::factory()->user->create( array( 'role' => 'administrator' ) ) ); + + $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='" . admin_url( 'tools.php' ) . "'", $actual ); + } + + /** + * @ticket 56302 + */ + public function test_wp_menu_output_uses_absolute_submenu_urls() { + 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' ), + ); + $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='" . 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::factory()->user->create( array( 'role' => 'administrator' ) ) ); + + 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='" . admin_url( 'admin.php?page=test-page' ) . "'", $actual ); + } + + /** + * @ticket 56302 + */ + public function test_wp_menu_output_preserves_external_urls() { + 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' ), + ); + + $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; + + $globals = $this->backup_globals( array( 'self', 'parent_file', 'submenu_file', 'plugin_page', 'typenow' ) ); + + $self = 'index.php'; + $parent_file = ''; + $submenu_file = ''; + $plugin_page = null; + $typenow = ''; + + ob_start(); + try { + _wp_menu_output( $menu, $submenu, $submenu_as_parent ); + return ob_get_clean(); + } finally { + $this->restore_globals( $globals ); + } + } + + private function require_menu_header() { + if ( function_exists( '_wp_menu_output' ) ) { + return; + } + + 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 = ''; + $submenu_file = ''; + + $_SERVER['PHP_SELF'] = '/wp-admin/index.php'; + + ob_start(); + 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 ] ); + } + } + } +}