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);
}
}
}