Display navigation menu item conditionally based on user capabilities

I have one “branch” of my main site’s navigation tree that should only be accessible to a set of registered, logged-in users. I understand how to query a user’s role and capabilities. This question is specifically about what is the best way to leverage the build-in nav menu, but just hide an item conditionally.

Do I need to overload the built-in default navigation and write a custom query and build the navigation structure manually? I’d love to avoid this if possible.

Read More

I don’t need full code samples, just your ideas and general framework / approach.

Appreciate the advice!

T

Related posts

Leave a Reply

6 comments

  1. The answer toscho posted is correct but for the few people who know what they are doing and how to do it. 🙂
    I’m adding this for the rest of the world as a simpler solution for the less advanced users. The idea would be to have different menus and just displaying them based on the user role.

    So say you have 3 menus named editor, author and default:

    if (current_user_can('Editor')){
        //menu for editor role
        wp_nav_menu( array('menu' => 'editor' ));
    
    }elseif(current_user_can('Author')){
        //menu for author role
        wp_nav_menu( array('menu' => 'author' ));
    
    }else{
        //default menu
        wp_nav_menu( array('menu' => 'default' ));
    }
    
  2. The trouble with overriding start_el and end_el is that doing so only controls the display of the menu item in question – it doesn’t affect the display of the children. I think you need to override display_element to hide the children as well.

    Also, it’s possible to use the description field of the menu item to hold information for each menu item about whether to display it or not.

    This code looks in the description of each menu item for a comma delimited list of capabilities such as [capability: this_one, next_one] and if the current user has none of the capabilities won’t display the item (or any of its children). It’s fairly easy to remove the strings from the description if you actually want to use the description for its intended purpose.

    /*
     * hide or display menus based on user capabilities
     *
     * Use the description field of the custom menu item. If it contains a string like [capability: xx, xx] 
     * display the menu item only if the user has at least one of the capabilities.
     * If a menu item is not displayed, nor are any of its sub items.
     */
    /* Custom Walker */
    class NASS_Nav_Walker extends Walker_Nav_Menu {
    
            // override parent method
            function display_element( $element, &$children_elements, $max_depth, $depth=0, $args, &$output ) {
                // we know the $element is a menu item  
                // we want to check its description field to see if it's OK to display it
                // in which case we just retreat to the parent method
                $desc = $element->description;
                if ( should_display_menu_item($desc) ) {
                    parent::display_element( $element, &$children_elements, $max_depth, $depth=0, $args, &$output );
                } else {
                    return;
                }
            }    
    }
    /* should we display the menu item, if this is its description? */
    function should_display_menu_item( $desc ) {
        // first see if the description contains a capability specification of the form
        // [capability: comma, delimited, list]
        // we assume all capability names consist only of lower case with underbars
        $prefix = "[capability:";
        $postfix = "]";
        $pattern = '@' . $prefix . '([a-z_, ]+)' . $postfix . '@';
        $answer = true;
        if ( preg_match($pattern, $desc, $matches) ) { // if we've got a match
            $found = $matches[1];   // the parenthesized bit of the regex
            $caps = array_map('trim', explode(",", $found));
            if ( count ($caps) > 0 ) { // there is at least one
                $answer = false;
                // now see if the user has any of them
                foreach ($caps as $cap) {
                    if ( current_user_can ($cap) ) $answer = true;
                }
            }
        }
        return $answer;
    
    }
    
  3. I have tried my hand at using the description field to use as to which roles can access which menu items, and based my modifications on code I got from here – Pimp my WP Menu

    My modified version:

    <?php
    
    /***
    * Menu WALKER - for restricting the menu items visibility
    * Code modified by - Trupti Bhatt (http://3sided.co.in)
    * using original code posted here - http://www.tisseur-de-toile.fr/wordpress-tricks/pimp-my-wordpress-menu-part-2-access-granted-to-authorized-personnel-only.html
    ***/
    class description_walker extends Walker_Nav_Menu
    {
            /*
                     *      Custom var to store current role
                     */
                    private $current_user_role = "";
    
                    /*
                     *      Get te current user role
                     */
                    private function getCurrentUserRole()
                    {
                                    global $current_user;
                                    if ( is_user_logged_in() )
                                    {
                                            if ( $this->current_user_role == "" )
                                            {
                                                    $this->current_user_role = $current_user->roles[0];
                                            }
    
                                            return $this->current_user_role;
                                    }
                                    else
                                    {
                                            $this->current_user_role='visitor';
                                            return $this->current_user_role;
                                    }
                    }
    
                    /*
                     *      Check if the user is an administrator
                     */
                    private function isAdmin()
                    {
                                    $current_role = $this->getCurrentUserRole();
    
                    if ( $current_role == "administrator" )
                                    {
                                                    return true;
                                    }
                                    else
                                    {
                                                    return false;
                                    }
                    }
    
                    /*
                     *      Get all restrictions
                     */
                    private function getAllRestrictions()
                    {
                            global $menu_restricted_access_array;
    
    
                                    $all_restrictions_array = array();
    
                                    foreach ( $menu_restricted_access_array as $one_restriction )
                                    {
                                            $all_restrictions_array = array_merge($all_restrictions_array, $one_restriction);
                                    }
                                    $all_restrictions_array = array_unique($all_restrictions_array);
    
                                    return $all_restrictions_array;
                    }
    
                    /*
                     *      Check the access
                     */
                    private function isAccessGranted( $id_menu_item )
                    {
                                    global $menu_restricted_access_array;
    
                    if ( $this->isAdmin() )
                                    {
                                                    return true;
                                    }
                    else if ( isset($menu_restricted_access_array[$this->current_user_role]) )
                    {
                        $restricted_access = $menu_restricted_access_array[$this->current_user_role];
    
                        if ( in_array($id_menu_item, $restricted_access) )
                                            {
                            return true;
                                            }
                                            else
                                            {
                            return false;
                                            }
                    }
                    else {
                            return true;
                                            }
    
                    }
    
         /*
                     *      Element render
                     */
                    function start_el(&$output, $item, $depth, $args)
            {
    
                global $wp_query, $menu_restricted_access_array;
                global $g_role,$g_pageid;
                $indent = ( $depth ) ? str_repeat( "t", $depth ) : '';
                            $g_role=strtolower((trim($item->description)));
    
                            $str = explode(',',$g_role);
                            for( $i=0; $i< count($str); $i++)
                                    {                      
    
                                            if (strtolower(trim($str[$i]))==$this->current_user_role)
                                            {                      
                                                    $restriction =$item->object_id;        
                                                    $menu_restricted_access_array[$this->current_user_role] =array( $restriction);
                                            }
    
    
                                    }
    
    
                $class_names = $value = '';
    
                $classes = empty( $item->classes ) ? array() : (array) $item->classes;
                $classes[] = 'menu-item-' . $item->ID;
    
    
                /*
                 *  First test, add custom class to each menu item
                 */
                    $classes[] = 'my-custom-menu-class';
    
                /*
                 *  Detect the menu item matching the unpublished page
                 *  Detect the menu item matching the unpublished page
                 */
                    // -> FLag to display the output
                    $item_to_display = true;
                    $is_item_published = true;
    
                    // -> Gather data of linked object
                    $item_data = get_post($item->object_id);
    
                    // --> If it's a page, act on the flag
    
                    if ( !empty($item_data) && ($item->object == "page") )
                    {
                        $is_item_published = ( $item_data->post_status == "publish" ) ? true : false;
                        $item_output = "";
                    }
    
                /*
                 *  Detect and display by user Role
                 **/
                    if ( _USE_RESTRICTED_ACCESS )
                    {
                        $restrictions_array = $this->getAllRestrictions();
                        $this->isAccessGranted($item->object_id);
    
                    }
                    else
                    {
                        $item_to_display = $is_item_published;
                    }
    
                $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item ) );
                $class_names = ' class="' . esc_attr( $class_names ) . '"';
    
    
    
                $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );
                $id = strlen( $id ) ? ' id="' . esc_attr( $id ) . '"' : '';
    
                $output .= $indent . '<li id="menu-item-'. $item->ID . '"' . $value . $class_names .'>';
    
                $attributes  = ! empty( $item->attr_title ) ? ' title="'  . esc_attr( $item->attr_title ) .'"' : '';
                $attributes .= ! empty( $item->target )     ? ' target="' . esc_attr( $item->target     ) .'"' : '';
                $attributes .= ! empty( $item->xfn )        ? ' rel="'    . esc_attr( $item->xfn        ) .'"' : '';
                $attributes .= ! empty( $item->url )        ? ' href="'   . esc_attr( $item->url        ) .'"' : '';
    
    
    
                            if($depth != 0)
                            {
                        $description = $append = $prepend = "";
                            }
    
                // --> If the flag is true, when display the output
    
                if ( $item_to_display )
                {
                    $item_output = $args->before;
                    $item_output .= '<a'. $attributes .'>';
                    $item_output .= $args->link_before .apply_filters( 'the_title', $item->title, $item->ID ).$append; // this is where the strong tags are prepend and append to the description
    
                                    $item_output .= '</a>';
                    $item_output .= $args->after;
                }
    
                $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
            }
    }
    /*
     *      Restrictions configuration
    * 2 is page id of Homepage
     **/
    define("_USE_RESTRICTED_ACCESS", true);
    $menu_restricted_access_array['subscriber'] = array('2');
    
    ?>
    

    It is not the cleanest version yet, but it works. I hope someone else can make a good use of it as well.

  4. I’m going to post my solution for others who may happen upon this thread. I’m not 100% happy with it because you shouldn’t couple a template with menu functionality (Toscho’s approach of using metadata or a custom taxonomy is probably more correct). However, it’s a quick and dirty one. I’ve tried to mitigate the tight coupling by providing some constants near the top of functions.php to help future developers maintain the code:

    // define the custom password-protected template that is used to determine whether this item is protected or not in the menus
    define ('ZG_PROTECTED_PAGE_TEMPLATE', 'page-membersonly.php');
    // define the custom capability name for protected pages
    define ('ZG_PROTECTED_PAGE_CAPABILITY', 'view_member_pages');
    

    So the protected page template is just a variant of single.php, but it will check to see whether current_user_can(ZG_PROTECTED_PAGE_CAPABILITY) before displaying any content, for security reasons.

    Next, I implement a custom walker, as per Toscho’s suggestion. The walker in this case is extremely simple – we override the Walker_Nav_Menu’s public start_el and end_el methods, just intercepting them long enough to ask the question: do we have access to see the menu item?

    The rule is simple: if the page is not a “private” page (which in this case is determined by that page using a particular page template) it’s visible. If it IS a “private” page, and the user is authenticated into a role that has the custom capability we’re looking for, then it is visible. Otherwise, it’s not visible.

    If we do have access, then we just have to use the Walker_Nav_Menu’s built-in methods without modification, so we call the parent:: method of the same name.

    /* Custom Walker to prevent password-protected pages from appearing in the list */
        class HALCO_Nav_Walker extends Walker_Nav_Menu {
    
            protected $is_private = false;
            protected $page_is_visible = false;
    
            // override parent method
            function start_el(&$output, $item, $depth, $args) {
                // does this menu item refer to a page that is using our protected template?
                $is_private = get_post_meta($item->object_id, '_wp_page_template', true) == ZG_PROTECTED_PAGE_TEMPLATE;
                $page_is_visible = !$is_private || ($is_private && current_user_can(ZG_PROTECTED_PAGE_CAPABILITY));
    
                if ($page_is_visible){
                    parent::start_el(&$output, $item, $depth, $args);
                }
            }
    
            // override parent method
            function end_el(&$output, $item, $depth) {
                if ($page_is_visible){
                    parent::end_el(&$output, $item, $depth);
                }
            }
        }