Hide Theme options and Customize Admin menu

Direct answer:

add_action( 'admin_menu', function() {
    global $current_user;
    $current_user = wp_get_current_user();
    $user_name = $current_user->user_login;

        //check condition for the user means show menu for this user
        if(is_admin() &&  $user_name != 'USERNAME') {
            //We need this because the submenu's link (key from the array too) will always be generated with the current SERVER_URI in mind.
            $customizer_url = add_query_arg( 'return', urlencode( remove_query_arg( wp_removable_query_args(), wp_unslash( $_SERVER['REQUEST_URI'] ) ) ), 'customize.php' );
            remove_submenu_page( 'themes.php', $customizer_url );
   }
}, 999 );

You have to respect the full-path naming.


The long answer, this:

add_action( 'admin_menu', function() {
    $page = remove_submenu_page( 'themes.php', 'customize.php' );
}, 999 );

Doesn’t work. But this:

add_action( 'admin_menu', function() {
    $page = remove_submenu_page( 'themes.php', 'widgets.php' );
}, 999 );

Works. If we just look at the links, then we can see that it should work, but check this out. In wp-admin/menu.php, line 164, we have:

$submenu['themes.php'][6] = array( __( 'Customize' ), 'customize', esc_url( $customize_url ), '', 'hide-if-no-customize' );

If we comment this out, bang, the Customize link dissapears, but if we go on ahead and paste this code and go to our wp-admin/index.php, we can see that there’s no customize.php in the array:

add_action( 'admin_menu', function() {
   global $submenu;
   var_dump( $submenu );
}, 999 );

The others are here. So what’s going on? If we dump the $submenu right after it’s created, we can see that it’s there:

[themes.php] => Array
    (
        [5] => Array
            (
                [0] => Themes
                [1] => switch_themes
                [2] => themes.php
            )

        [6] => Array
            (
                [0] => Customize
                [1] => customize
                [2] => customize.php?return=%2Fwordpress%2Fwp-admin%2Findex.php
                [3] => 
                [4] => hide-if-no-customize
            )

        [10] => Array
            (
                [0] => Menus
                [1] => edit_theme_options
                [2] => nav-menus.php
            )

    )

…so, somewhere, it gets lost, or so you would think, at the end of this file, we see that, yet again, we include another file:

require_once(ABSPATH . 'wp-admin/includes/menu.php');

If we then go at the end of this file and we dump the $menu, what we get is..surprisingly, a menu without ‘customizer’ in it. But it was there just moments ago.

Surprisingly, if we do:

add_action( 'admin_menu', function() {
    global $submenu;
    var_dump( $submenu );
}, 999 );

It’s clear, the customize.php is there…except…it generates itself with some parameters after: customize.php?return=%2Fwordpress%2Fwp-admin%2Findex.php (this depends on your site).

Interesting, so if we do:

add_action( 'admin_menu', function() {
    remove_submenu_page( 'themes.php', 'customize.php?return=%2Fwordpress%2Fwp-admin%2Findex.php' );
}, 999 )

Or, in your case:

add_action( 'admin_menu', function() {
    remove_submenu_page( 'themes.php', 'customize.php?return=%2Fwp-admin%2Findex.php' );
}, 999 )

It works.

So what did we learn today? WP Core is a bad piece of crap, ok, it’s true but it’s your mistake for not looking at what remove_submenu_page looks for.

function remove_submenu_page( $menu_slug, $submenu_slug ) {
    global $submenu;

    if ( !isset( $submenu[$menu_slug] ) )
        return false;

    foreach ( $submenu[$menu_slug] as $i => $item ) {
        if ( $submenu_slug == $item[2] ) {
            unset( $submenu[$menu_slug][$i] );
            return $item;
        }
    }

    return false;
}

It first checks to see if the menu exists, in our case themes.php and then it goes over each item from, say, Appearance, each being an array itself, then it looks for the third item, which corresponds to the [2] => customize.php... item, so, of course it’s not going to find our customize.php, that’s why we need to provide it the full link. It’s just a simple mistake.