Ever come across a plugin that is using the manage_options capability for a page that... really doesn't need to be? Well, I've come across just that.

This may be more of a general question about hooking into add_submenu_page, so not just specific to my use-case.

I looked at add_dashboard_page which is simply a wrapper for add_submenu_page:

function add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, $menu_slug, $function = '' ) {
    global $submenu;
    global $menu;
    global $_wp_real_parent_file;
    global $_wp_submenu_nopriv;
    global $_registered_pages;
    global $_parent_pages;

    $menu_slug = plugin_basename( $menu_slug );
    $parent_slug = plugin_basename( $parent_slug);

    if ( isset( $_wp_real_parent_file[$parent_slug] ) )
        $parent_slug = $_wp_real_parent_file[$parent_slug];

    if ( !current_user_can( $capability ) ) {
        $_wp_submenu_nopriv[$parent_slug][$menu_slug] = true;
        return false;

    // If the parent doesn't already have a submenu, add a link to the parent
    // as the first item in the submenu. If the submenu file is the same as the
    // parent file someone is trying to link back to the parent manually. In
    // this case, don't automatically add a link back to avoid duplication.
    if (!isset( $submenu[$parent_slug] ) && $menu_slug != $parent_slug ) {
        foreach ( (array)$menu as $parent_menu ) {
            if ( $parent_menu[2] == $parent_slug && current_user_can( $parent_menu[1] ) )
                $submenu[$parent_slug][] = $parent_menu;

    $submenu[$parent_slug][] = array ( $menu_title, $capability, $menu_slug, $page_title );

    $hookname = get_plugin_page_hookname( $menu_slug, $parent_slug);
    if (!empty ( $function ) && !empty ( $hookname ))
        add_action( $hookname, $function );

    $_registered_pages[$hookname] = true;
    // backwards-compatibility for plugins using add_management page. See wp-admin/admin.php for redirect from edit.php to tools.php
    if ( 'tools.php' == $parent_slug )
        $_registered_pages[get_plugin_page_hookname( $menu_slug, 'edit.php')] = true;

    // No parent as top level
    $_parent_pages[$menu_slug] = $parent_slug;

    return $hookname;

It honestly doesn't look like there's anything I can actually plug into with an existing dashboard page to change the capability. So I'm trying to decide if I'm better off using remove_submenu_page and then attempt to redeclare that same submenu. I know there will be things I need to look out for (if the display function used for the page has anything that is additionally checking for the capability or displaying anything vital to the site). Always helpful to have a second pair of eyes on these so I don't over complicate things. Thanks all!


Thanks to both @toscho and @userabuser here's what I got:

function wpse_71303_change_menu_cap()
    global $submenu;
    foreach ($submenu['index.php'] as $dashboard => $key) {
        if ($key[0] == 'Analytics360°') {
            $submenu['index.php'][$dashboard][1] = 'analytics';
add_action( 'admin_head', 'wpse_71303_change_menu_cap' );

If I run a print_r($submenu) I do see the new capability - but I still can't access the menu item under the client role I created (with theanalytics_360` capability) (used the Members plugin to create). Possibly firing too late? Definitely a bit odd. Thanks as always!

The code from Update works for anyone that comes across this. It was an unneeded check around add_dashboard_page() giving me the issue.

  • What's the problem? You're not specifically stating the use-case/scenario/question. You want to remove a submenu page that is using the manage_options capability? Really doesn't need to be under what context? Curious here...
    – Adam
    Nov 2, 2012 at 13:49
  • A plugin I'm using has manage_options declared as a capability for accessing a plugin page (admin) - I'm looking to see if we can hook into add_submenu_page and change that capability, or if it's better to use remove_submenu_page and then just re-add it.
    – Zach
    Nov 2, 2012 at 14:02
  • 2
    As per @toscho answer. I was doing something similar the other date, however specifically altering positions of submenu items. If you var_dump($menu) and or $submenu - you can see what array keys hold which properties. Now I am curious though what option performs better? remove_submenu_page then re-add or just directly modify the global var array. The latter sounds like it would be the case but then whats best practice? In any case, his answer is correct.
    – Adam
    Nov 2, 2012 at 14:24
  • Appreciate your thoughts on this - the performance comparison would be interesting.
    – Zach
    Nov 2, 2012 at 14:32
  • 1
    I don't believe you have granted proper permissions up the chain. That might be the issue where a certain permission, despite that of what you set, is taking precedence no matter what.
    – Adam
    Nov 3, 2012 at 17:01

2 Answers 2


Hook into admin_head, the last action before the menu is rendered, and change the global $menu:

add_action( 'admin_head', 'wpse_71303_change_menu_cap' );

 * Change the capability to access an admin menu item.
 * @wp-hook admin_head
 * @return void
function wpse_71303_change_menu_cap()
    global $menu;

    foreach ( $menu as $key => $item )
        // Find menu by name
        if ( 'Tools' === $item[0] ) // default cap: 'edit_posts'
            $menu[ $key ][1] = 'new_capability';
  • Hey @toscho thanks for the response - definitely getting me off on the right foot. I'm actually looking to hook into $submenu which seems to be giving me some trouble (inside of index.php) - better explained here. Have any thoughts? Thanks!
    – Zach
    Nov 2, 2012 at 15:10
  • The problem there seems to be they are using index.php as a key without it actually be a string - so I'm having trouble accessing it correctly.
    – Zach
    Nov 2, 2012 at 15:19
  • @Zach Yeah that's right, that's not an error per say. So it goes a little like this $submenu['index.php'][0], then $submenu['index.php'][1] etc. That's how you access the properties of the array. See my link for further example too in OP comments. You are on right track.
    – Adam
    Nov 2, 2012 at 18:58
  • @userabuser Updated my question with progress -- we've got to be almost there :) It seems to attach the new capability correctly, but isn't allowing a role that has that capability to then access the page.
    – Zach
    Nov 2, 2012 at 19:53

Putting together the earlier answer as well as the original poster's update, here's what worked for me:

Note that the code below happens to be for the CiviCRM plugin. It changes the access permissions from access_civicrm to administer_civicrm for the "Settings" menu item, since I don't want regular users to access that. In my case, rather than inspect the submenu name in English, I'm inspecting the slug for the page civi_options (i.e. civi_options.php) that is invoked. Also, I found it necessary to set $submenu['CiviCRM'][$key] = $item; to update the $submenu structure.

Also note that the method below is not true security! It suppresses the submenu item, but it does not prevent access to the submenu if the user knows the correct URL.

// ...this goes in my class `init()` function...
    add_action( 'admin_head', array( $this, 'cv_adjust_submenu_permissions') );

// ...and then later in the body of the class...

    function cv_adjust_submenu_permissions() {
        global $submenu;
        //error_log(print_r($submenu, true));
        foreach ( $submenu['CiviCRM'] as $key => $item ) {
            // change CiviCRM... Settings submenu item capability
            if ( 'civi_options' === $item[2] ) { // default capability: 'access_civicrm'
                $item[1] = 'administer_civicrm'; // new capability
                $submenu['CiviCRM'][$key] = $item;

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.