Remove WordPress Admin Menu Without Affecting WordPress Core System

In WordPress, each user type have different capability. Sometimes, we want to change these capability and the most easiest way to do that is to remove what they can see when they logged in. Especially when someone wants to change WordPress into a powerful CMS and remove certain admin menu without touching the core system. In most CMS, there are so much restriction on their core system that makes it really inflexible. Unlike other system, WordPress is able to modify their core codes using plugin without affecting the fundamental codes in WordPress. Hence, we can continue to upgrade our system without having to worry about updates that will kill your modification. In this article, i will show you how i remove WordPress admin menu using plugin style without affecting WordPress Core codes.

WordPress Admin Menu

If you are able to dig into WordPress code, you will notice that their menu is created by a single function using two global variables as parameter. You can easily find this code located at wp-admin/menu-header.php, line 157-158.

_wp_menu_output( $menu, $submenu );
do_action( 'adminmenu' );

From the look of the method, you would have easily guess that this method is also a global method which takes in global variables $menu and $submenu to construct a full flag admin menu in WordPress. However, this method is like a loop that takes in a variable and loop through whatever is contain in the variable given. Hence, we will have to look at how each global variable is built to determine how to properly remove a WordPress admin menu.

Global Variable - $menu

If you dig deeper into WordPress, you will notice that the global variable $menu and $submenu are located at wp-admin/menu.php, line 25 onwards. This two variables play an important part in our objective as they create and remove main and submenu in WordPress. If you look at the code from line 28-115, you will notice that both menu and submenu is constructed first regardless of permission.

$menu[0] = array( __('Dashboard'), 'read', 'index.php', '', 'menu-top', 'menu-dashboard', 'div' );

$menu[4] = array( '', 'read', 'separator1', '', 'wp-menu-separator' );

$menu[5] = array( __('Posts'), 'edit_posts', 'edit.php', '', 'open-if-no-js menu-top', 'menu-posts', 'div' );
	$submenu['edit.php'][5]  = array( __('Edit'), 'edit_posts', 'edit.php' );
	/* translators: add new post */
	$submenu['edit.php'][10]  = array( _x('Add New', 'post'), 'edit_posts', 'post-new.php' );

	$i = 15;
	foreach ( $wp_taxonomies as $tax ) {
		if ( $tax->hierarchical || ! in_array('post', (array) $tax->object_type, true) )
			continue;

		$submenu['edit.php'][$i] = array( esc_attr($tax->label), 'manage_categories', 'edit-tags.php?taxonomy=' . $tax->name );
		++$i;
	}

	$submenu['edit.php'][50] = array( __('Categories'), 'manage_categories', 'categories.php' );

$menu[10] = array( __('Media'), 'upload_files', 'upload.php', '', 'menu-top', 'menu-media', 'div' );
	$submenu['upload.php'][5] = array( __('Library'), 'upload_files', 'upload.php');
	/* translators: add new file */
	$submenu['upload.php'][10] = array( _x('Add New', 'file'), 'upload_files', 'media-new.php');

$menu[15] = array( __('Links'), 'manage_links', 'link-manager.php', '', 'menu-top', 'menu-links', 'div' );
	$submenu['link-manager.php'][5] = array( __('Edit'), 'manage_links', 'link-manager.php' );
	/* translators: add new links */
	$submenu['link-manager.php'][10] = array( _x('Add New', 'links'), 'manage_links', 'link-add.php' );
	$submenu['link-manager.php'][15] = array( __('Link Categories'), 'manage_categories', 'edit-link-categories.php' );

$menu[20] = array( __('Pages'), 'edit_pages', 'edit-pages.php', '', 'menu-top', 'menu-pages', 'div' );
	$submenu['edit-pages.php'][5] = array( __('Edit'), 'edit_pages', 'edit-pages.php' );
	/* translators: add new page */
	$submenu['edit-pages.php'][10] = array( _x('Add New', 'page'), 'edit_pages', 'page-new.php' );

$menu[25] = array( sprintf( __('Comments %s'), "<span id='awaiting-mod' class='count-$awaiting_mod'><span class='pending-count'>" . number_format_i18n($awaiting_mod) . "</span></span>" ), 'edit_posts', 'edit-comments.php', '', 'menu-top', 'menu-comments', 'div' );

$_wp_last_object_menu = 25; // The index of the last top-level menu in the object menu group

$menu[59] = array( '', 'read', 'separator2', '', 'wp-menu-separator' );

$menu[60] = array( __('Appearance'), 'switch_themes', 'themes.php', '', 'menu-top', 'menu-appearance', 'div' );
	$submenu['themes.php'][5]  = array(__('Themes'), 'switch_themes', 'themes.php');
	$submenu['themes.php'][10] = array(__('Editor'), 'edit_themes', 'theme-editor.php');
	$submenu['themes.php'][15] = array(__('Add New Themes'), 'install_themes', 'theme-install.php');

$update_plugins = get_transient( 'update_plugins' );
$update_count = 0;
if ( !empty($update_plugins->response) )
	$update_count = count( $update_plugins->response );

$menu[65] = array( sprintf( __('Plugins %s'), "<span class='update-plugins count-$update_count'><span class='plugin-count'>" . number_format_i18n($update_count) . "</span></span>" ), 'activate_plugins', 'plugins.php', '', 'menu-top', 'menu-plugins', 'div' );
	$submenu['plugins.php'][5]  = array( __('Installed'), 'activate_plugins', 'plugins.php' );
	/* translators: add new plugin */
	$submenu['plugins.php'][10] = array(_x('Add New', 'plugin'), 'install_plugins', 'plugin-install.php');
	$submenu['plugins.php'][15] = array( __('Editor'), 'edit_plugins', 'plugin-editor.php' );

if ( current_user_can('edit_users') )
	$menu[70] = array( __('Users'), 'edit_users', 'users.php', '', 'menu-top', 'menu-users', 'div' );
else
	$menu[70] = array( __('Profile'), 'read', 'profile.php', '', 'menu-top', 'menu-users', 'div' );

if ( current_user_can('edit_users') ) {
	$_wp_real_parent_file['profile.php'] = 'users.php'; // Back-compat for plugins adding submenus to profile.php.
	$submenu['users.php'][5] = array(__('Authors & Users'), 'edit_users', 'users.php');
	$submenu['users.php'][10] = array(__('Add New'), 'create_users', 'user-new.php');
	$submenu['users.php'][15] = array(__('Your Profile'), 'read', 'profile.php');
} else {
	$_wp_real_parent_file['users.php'] = 'profile.php';
	$submenu['profile.php'][5] = array(__('Your Profile'), 'read', 'profile.php');
}

$menu[75] = array( __('Tools'), 'read', 'tools.php', '', 'menu-top', 'menu-tools', 'div' );
	$submenu['tools.php'][5] = array( __('Tools'), 'read', 'tools.php' );
	$submenu['tools.php'][10] = array( __('Import'), 'import', 'import.php' );
	$submenu['tools.php'][15] = array( __('Export'), 'import', 'export.php' );
	$submenu['tools.php'][20] = array( __('Upgrade'), 'install_plugins',  'update-core.php');

$menu[80] = array( __('Settings'), 'manage_options', 'options-general.php', '', 'menu-top', 'menu-settings', 'div' );
	$submenu['options-general.php'][10] = array(__('General'), 'manage_options', 'options-general.php');
	$submenu['options-general.php'][15] = array(__('Writing'), 'manage_options', 'options-writing.php');
	$submenu['options-general.php'][20] = array(__('Reading'), 'manage_options', 'options-reading.php');
	$submenu['options-general.php'][25] = array(__('Discussion'), 'manage_options', 'options-discussion.php');
	$submenu['options-general.php'][30] = array(__('Media'), 'manage_options', 'options-media.php');
	$submenu['options-general.php'][35] = array(__('Privacy'), 'manage_options', 'options-privacy.php');
	$submenu['options-general.php'][40] = array(__('Permalinks'), 'manage_options', 'options-permalink.php');
	$submenu['options-general.php'][45] = array(__('Miscellaneous'), 'manage_options', 'options-misc.php');

Furthermore, its being done neat and nicely. After that a few loop is conducted to remove the menu and submenu according to the user permission. You can see that on line 152 - 209.

$_wp_submenu_nopriv = array();
$_wp_menu_nopriv = array();
// Loop over submenus and remove pages for which the user does not have privs.
foreach ( array( 'submenu' ) as $sub_loop ) {
	foreach ($$sub_loop as $parent => $sub) {
		foreach ($sub as $index => $data) {
			if ( ! current_user_can($data[1]) ) {
				unset(${$sub_loop}[$parent][$index]);
				$_wp_submenu_nopriv[$parent][$data[2]] = true;
			}
		}

		if ( empty(${$sub_loop}[$parent]) )
			unset(${$sub_loop}[$parent]);
	}
}

// Loop over the top-level menu.
// Menus for which the original parent is not acessible due to lack of privs will have the next
// submenu in line be assigned as the new menu parent.
foreach ( $menu as $id => $data ) {
	if ( empty($submenu[$data[2]]) )
		continue;
	$subs = $submenu[$data[2]];
	$first_sub = array_shift($subs);
	$old_parent = $data[2];
	$new_parent = $first_sub[2];
	// If the first submenu is not the same as the assigned parent,
	// make the first submenu the new parent.
	if ( $new_parent != $old_parent ) {
		$_wp_real_parent_file[$old_parent] = $new_parent;
		$menu[$id][2] = $new_parent;

		foreach ($submenu[$old_parent] as $index => $data) {
			$submenu[$new_parent][$index] = $submenu[$old_parent][$index];
			unset($submenu[$old_parent][$index]);
		}
		unset($submenu[$old_parent]);

		if ( isset($_wp_submenu_nopriv[$old_parent]) )
			$_wp_submenu_nopriv[$new_parent] = $_wp_submenu_nopriv[$old_parent];
	}
}

do_action('admin_menu', '');

// Remove menus that have no accessible submenus and require privs that the user does not have.
// Run re-parent loop again.
foreach ( $menu as $id => $data ) {
	// If submenu is empty...
	if ( empty($submenu[$data[2]]) ) {
		// And user doesn't have privs, remove menu.
		if ( ! current_user_can($data[1]) ) {
			$_wp_menu_nopriv[$data[2]] = true;
			unset($menu[$id]);
		}
	}
}

Now we have a basic understanding of how WordPress handle their admin menu according to user access. We are ready to remove or modify any user access to alter WordPress user capability to view a particular menu in WordPress.

Removing WordPress Admin Menu

After having you wasting your time reading all the way from the top to here, i finally getting back to track and write what this article is about. We understand from all the rubbish on top that the global variable $menu array contains all the top level menu item and the global variable $submenu array contains all submenu page of each top level menu item. We can perform a few methods to remove a WordPress Admin Menu. The first simple and very basic way of removing a WordPress admin menu is to unset the menu resist in the global array.

function remove_submenu() {
global $submenu;
//remove Theme editor
unset($submenu['themes.php'][10]);
}

function remove_menu() {
global $menu;
//remove post top level menu
unset($menu[5]);
}
add_action('admin_head', 'remove_menu');
add_action('admin_head', 'remove_submenu');

We can remove a set of admin menu by doing this.

function remove_menus () {
global $menu;
		$restricted = array(__('Dashboard'), __('Posts'), __('Media'), __('Links'), __('Pages'), __('Appearance'), __('Tools'), __('Users'), __('Settings'), __('Comments'), __('Plugins'));
		end ($menu);
		while (prev($menu)){
			$value = explode(' ',$menu[key($menu)][0]);
			if(in_array($value[0] != NULL?$value[0]:"" , $restricted)){unset($menu[key($menu)]);}
		}
}
add_action('admin_menu', 'remove_menus');

The above code can be further reduce by 4 lines but i will keep it as it is.

This is pretty simple but not all user have these access and you might want to do some checking before accessing an invalid array to be unset which might give an error to be display. Although this is simple and the objective is achieve by removing the display of the menu/submenu, the user will still be able to direct access via the url. Hence, we need something better. Something that will disable all admin menu and sub menu tab from accessing and viewing.

function remove_menus () {
global $menu, $submenu, $user_ID;
	$the_user = new WP_User($user_ID);
	$valid_page = "admin.php?page=contact-form-7/admin/admin.php";
	$restricted = array('index.php','edit.php','categories.php','upload.php','link-manager.php','edit-pages.php','edit-comments.php', 'themes.php', 'plugins.php', 'users.php', 'profile.php', 'tools.php', 'options-general.php');
	$restricted_str = 'widgets.php';
	end ($menu);
	while (prev($menu)){
		$menu_item = $menu[key($menu)];
		$restricted_str .= '|'.$menu_item[2];
		if(in_array($menu_item[2] , $restricted)){
			$submenu_item = $submenu[$menu_item[2]];
			if($submenu_item != NULL){
				$tmp = $submenu_item;
				$max = array_pop(array_keys($tmp));
				for($i = $max; $i > 0;$i-=5){

					 if($submenu_item[$i] != NULL){
						$restricted_str .= '|'.$submenu[$menu_item[2]][$i][2];
						unset($submenu[$menu_item[2]][$i]);
					}
				}
			}
			unset($menu[key($menu)]);
		}
	}
	$result = preg_match('/(.*?)\/wp-admin\/?('.$restricted_str.')??(('.$restricted_str.'){1})(.*?)/',$_SERVER['REQUEST_URI']);
	if ($result != 0 && $result != FALSE){
		wp_redirect(get_option('siteurl') . '/wp-admin/' . $valid_page);
		exit(0);
	}
}
add_action('admin_menu', 'remove_menus');

The above function did just the thing. We will only required to provide the file name of the top level category and it will automatically disable all access to the subsequence admin sub menu. However, we will have to provide a valid page for user to gain access for the first time. The above code will disable ALL ADMIN MENU AND SUB MENU FOR ALL WORDPRESS ACCESS. The only page that was left accessible are the custom admin menu created by our user such as TweetMeme or Contact 7 form admin menu.

Easiest way to remove sub menu

After version 3.1, you are provided with the following methods to remove submenu

add_action( 'admin_menu', 'my_remove_menu_pages' );
function my_remove_menu_pages() {
        remove_menu_page('link-manager.php');
        //remove_menu_page('themes.php');
        remove_submenu_page( 'themes.php', 'themes.php' );
        remove_submenu_page( 'themes.php', 'theme-editor.php' );
        remove_submenu_page( 'themes.php', 'themes.php?page=custom-background' );
        remove_submenu_page( 'widgets.php', 'theme-editor.php' );
        remove_menu_page('tools.php');
        remove_menu_page('upload.php');
        remove_menu_page('edit-comments.php');
        remove_menu_page('plugins.php');
        remove_menu_page('admin.php?page=w3tc_general');
        remove_menu_page('admin.php?page=better_wp_security');
        remove_menu_page('admin.php?page=wpcf7');
        remove_submenu_page( 'index.php', 'update-core.php' );
        remove_submenu_page( 'options-general.php', 'options-discussion.php' );
        remove_submenu_page( 'options-general.php', 'options-writing.php' );
        remove_submenu_page( 'options-general.php', 'options-reading.php' );
        remove_submenu_page( 'options-general.php', 'options-permalink.php' );
        remove_submenu_page( 'options-general.php', 'options-media.php' );
}

Pretty simple and direct, first parameter is the file name or parent file name of the page you want to remove, second parameter is the submenu you wish to remove.

Concolusion

This is the way i use to remove admin menu in WordPress. We do not have to check for user access as the user will already be redirected to the specific page before they can enter the permission denial page. Have fun 🙂

13 thoughts on “Remove WordPress Admin Menu Without Affecting WordPress Core System

  1. Great post Clay - I find that I have to do this every once in a while, and before finding your post, I often just did it by using some custom css to hide different parts of the admin menu - definitely a poor solution compared to accessing the menu array directly. Thanks!

  2. Nice solution! But how would you remove only a couple of submenu items without removing the topmenu, say I'd like to remove the categories submenu from the Post top menu..

  3. It should be quite easy from the look of the code. You can just place a set of array and only unset those sub menu in the array (using in array). Then, remove the main menu 'unset' so that all main menu remain there.

  4. Is there any centralized place where I can change the links to the different admin panels?

    I'd like to change the unset/remove for all menus/submenus for a dummy panel, changing the urls to "#".

    Thanks to you now I know how the menus are added/removed.
    Do you know how I can replace the URL's for a few? (or the ones out of the privileges)

  5. I assumed, as you say, the menu is constructed with ALL the submenus first, and commented all the functions after that, and any other with the word "unset" in them, and the submenus are not rendered. I believe there should be a way to show them WITHOUT adding full capabilities to my subscriber.

    I just got the titles (e.g. "Settings") but nothing inside it.
    Also, I see the url is the element [2] in the submenus. Can I just change the value of the filename.php for each of them in one of the loops of your script to change the link to "#" in all the restricted ones?

  6. @Daniel, @Corey, @Nico : Welcome 🙂
    @sergio: I see you need some help there. Sorry for the late reply though. The core will add full capabilities to any user first and remove them according to your user access level permission. Once the core has finish all their work, our action hook will start acting up. Hence, it is not our control to prevent all the capabilities from adding upfront. [well, you can edit the WordPress core code if you want but our objection is not to touch them right 🙂 ]

    Its been ages since i look at this code.. let's see.. In short you want to remove ALL menu in your WordPress control panel by just adding the value # right? The answer is you can't do that unless you program a function to take account of all the files name available in a panel and remove them via the symbol #. My method above is just providing you a way to remove them by inserting all the file name into an array. Achieving what you want will need to customize the method a bit to meet your need. 🙂

  7. Thanks for the reply.
    I found the way to change the url for all of them. It's the [2] object in the array…
    now, I just must be missing something here.
    You say "…The core will add full capabilities to any user first…" Right?
    But in my case -WP 2.9.1- is not that way.
    In the FIRST pass, the core DOESN'T ADD the ones out of the user's capabilities.
    Was this post for a previous version only?
    Thanks.

  8. yup. Its a code i used on previous version. But this is an action hook so you don't really have to bother about what the core does. Furthermore, in 2.9.1, it seems like there are no changes on wp-admin/menu.php file. Hence, this should work the same as describe in this article.

    "In the FIRST pass, the core DOESN’T ADD the ones out of the user’s capabilities." Please look at wp-admin/menu.php on line 152 – 209 which explain what the core is doing on the menu level.

    Good luck 🙂

Comments are closed.