— tomauger.com

WordPress: hiding menu items from users based on their roles (using a custom walker)

Sometimes you may wish to have a section of your WordPress based site protected for exclusive access by logged-in users with a certain access level (in WordPress terminology: a role). I recently encountered this feature request on a client project, so I thought I’d share my results here, since I had to cobble together the information from a great variety of sources. Much thanks, in particular, to Toscho at the most excellent WordPress StackExchange site for setting me onto custom walkers as the way to customize the way your menu should display.

Since I’ll be limiting this blog post to my actual implementation, rather than a more general discussion of best practices, I should just quickly set up the particular business requirements I was dealing with:

  • I would only be protecting Pages, not Posts
  • The protected pages would only be accessed through a menu in the Footer of the site
  • If the user is logged in and has access to the pages, those pages should appear in the menu
  • If the user doesn’t have access to the pages, the menu item in the footer would be a Login link instead
  • Users that should have access to the pages are:  administrators, editors and a custom role (“members”)

Solution Overview

The situation, though not overly complex, requires a number of elements to be in place:

  1. A custom role called “Member” must be created. We can then assign this role to registered users so they get access to the pages.
  2. This role will get a custom capability called “view-member-pages”, which would be the actual check to see whether they can see the page. We tie the visibility to a capability, not a role, so that we can then also assign that capability to Administrators and Editors.
  3. A custom menu must be built that will have this special show/hide feature
  4. That custom menu will leverage an internal class called a Walker that is used to traverse a hierarchical structure (like a menu) and display the HTML (ul and li tags and all that) required
  5. We will override the default Nav_Menu_Walker class with our own that will intercept pages that ought to be protected and only show them under the right conditions, otherwise showing a login link.
  6. We have to make sure that if there are two or more pages that are hidden this way, we only show one login menu item, not one for each hidden page!
  7. Finally, I elected to make the criteria that determines whether a page is “protected” or not the actual template that the page uses, so we need to create that custom template and assign it to the pages we want protected.

The Custom Menu

This part is quite simple. We start by registering a new menu location with our theme. This allows the menu location to become available on the Admin > Appearance > Menus page so we can create the menu there and add whatever items we want to it. Of course, we still have to go into our Admin back-end and create the menu, and associate it with this new location.

Registering the menu location is done with register_nav_menu(). This is probably best done in functions.php:

register_nav_menu('footer', 'Footer menu');

Oooo. Scarrrry. I know, right? Real hard stuff. Keep reading, mister Smarty Pants.

And, then, in our footer.php file where we want this actual menu to appear, we actually call that menu using wp_nav_menu():

wp_nav_menu(array(
 'theme_location'  => 'footer',
 'container'       => 'div',
 'depth'           => 1,
 'walker'          => new ZG_Nav_Walker()
)    );

This particular menu is non-hierarchical, meaning it’s only one level. This explains the “depth” parameter. The “walker” parameter is where the magic starts to happen. Or at least, it’s letting the magic know where we want it to happen. The actual magic happens inside the custom ZG_Nav_Walker class that we’ll create a little later on.

Custom Roles and Capabilities

In order to allow certain users to see our pages, and others to just see the “login” link, we will leverage WordPress’ Roles and Capabilities. A Capability is used to enable or disable someone from doing something specific (like edit a post, or moderate a comment). A Role is just a grouping of these capabilities. Administrator, Editor and Subscriber are examples of existing roles.

Well, we don’t want our “Members” to have any special capabilities other than viewing these special pages, so it makes more sense to just create a custom Role called “Member” and assign a single capability (which we’ll call “view-member-pages”) to it.

First we go off and define a few constants at the top of our functions.php file, to make things a little easier to maintain. Plus I kinda like all those ALL_CAPS. Makes me feel important. Or impotent. Something like that.

// define the custom capability name for protected pages
 define ('ZG_PROTECTED_PAGE_CAPABILITY', 'view_member_pages');

Then we add the new role and capabilities (later on in functions.php)

function roles_and_capabilities(){
 // create a new role for Members
 add_role('member', 'Member', array(
 ZG_PROTECTED_PAGE_CAPABILITY => true
 ));
 
 // add the custom capability to other qualified roles
 get_role('administrator')->add_cap(ZG_PROTECTED_PAGE_CAPABILITY);
 get_role('editor')->add_cap(ZG_PROTECTED_PAGE_CAPABILITY);
}
add_action('wp_loaded', 'roles_and_capabilities');

We wrap the framework calls add_role() and WP_Role->add_cap inside a function so that we can invoke the function as an action at the appropriate time, just so we can stay idiomatic. That’s idiomatic, not idiotic, smart-ass.

Since we want Admins and Editors to also be able to see these protected pages, we have added our custom capability to these existing roles as well.

At this point, you’ll want to go into Admin and create a new user and see if you can assign the “Member” role to that user.

Using a Custom Walker to customize your WordPress nav menu

So, like the Google-search-optimized subhead here says, we’re now going to create the magic by extending the built-in Walker_Nav_Menu class with one of our own. The Walker construct is used to process a list of elements, one element at a time, and do something with each element. The concept was probably based on an iterator design pattern. Our custom 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?

If we do have access, we just call the base Walker_Nav_Menu’s start_el and end_el methods and be done with it. If we don’t have access, well, we change the output to show a link to the wp-login.php page. And we implement a little bit of business logic to make sure that if we have two or more protected pages, we don’t see multiple login links.

The criteria that I opted to go with here, to determine whether a page was a “protected” page or not was the template that we’ve associated with that page. I don’t love this approach, but it works for this client. I would actually recommend using some kind of custom metadata or a custom taxonomy (ie: access –> public, private, members etc). But I’ll leave that for you kids to figure out on your own. So the first thing I do is define another constant (again, everything in functions.php):

// 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');

Then we get to our class (the “magic”!!):

/* Custom Walker to prevent password-protected pages from appearing in the list */
class ZG_Nav_Walker extends Walker_Nav_Menu {
 
	protected $is_private = false;
	protected $page_is_visible = false;
	protected $login_shown = 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);
		} else if (! $login_shown){
			$item->ID = 'login';
			$item->attr_title = "";
			$item->target = "";
			$item->xfn = "";
			$item->url = site_url("wp-login.php");
			$item->title = __("Member's Login");
 
			parent::start_el(&$output, $item, $depth, $args);
		}
	}
 
	// override parent method
	function end_el(&$output, $item, $depth) {
		if ($page_is_visible || ! $login_shown){
			if (! $login_shown){
				$login_shown = true;
			}
 
			parent::end_el(&$output, $item, $depth);
		}
	}
}

The way you change what the Walker outputs is by editing the properties of the $item variable, which is an instance of the Menu object. I haven’t yet tracked down the Menu object definition in the source code(ed: actually, it turns out there is no Menu object, but the item itself is defined in nav-menus.php), so you can get a full list of all properties if you just do a print_r($item) – and be prepared for a lot of output. Would you believe that the entire body of your Post / Page is included not once, but twice inside that object. Yeesh. No wonder WordPress needs to much PHP RAM on the server. Oh well.

Of course, the one piece of information that you DO need – the page template associated with that menu item – is not actually available in this Menu object. Go figure. But we can use get_post_meta to grab that little tidbit.

Last Step

Finally, we just need to create our custom page template. We’ll once again use current_user_can(ZG_PROTECTED_PAGE_CAPABILITY) to make sure that we’re allowed to display the content. Then we associate that template with our protected pages, and test, test, test!

Hope this helps someone out there!

  • http://twitter.com/iamandyogden Andy Ogden

    I think your idea might help with a customisation I’m looking to do, but to be honest this post has just blown my mind during lunch and I’m going to have to sit dow later tonight and try digest this.

    Answers a few questions, but poses so many more, lol

    If you’re interested, here my topic on the WordPress forum

    Andy

  • http://www.tomauger.com Tom Auger

    Hi Andy. For some reason Disqus ate your link to the WordPress support forum. If you post it again, I’ll have a look…

  • KK

    Is there a plugin for this?

  • Tomaugerdotcom

    There are a few out there in the wild that do some, or all, of what my post is about. A quick Google search should pull them up.