Условное отображение пункта навигационного меню на основе прав пользователя
У меня есть одна "ветвь" навигационного дерева на основном сайте, которая должна быть доступна только определенной группе зарегистрированных и авторизованных пользователей. Я понимаю, как проверять роль и права пользователя. Этот вопрос конкретно о том, какой лучший способ использовать встроенное навигационное меню, но при этом скрывать элемент при определенных условиях.
Нужно ли мне переопределять встроенную навигацию по умолчанию и писать пользовательский запрос для ручного построения структуры навигации? Я бы хотел избежать этого, если возможно.
Мне не нужны полные примеры кода, просто ваши идеи и общий подход к решению.
Заранее благодарю за советы!
T

Используйте собственный обработчик и проверяйте права доступа перед созданием элемента.

Абсолютно блестяще. Спасибо за это - хотя я удивлен, что все настолько сложно. Как вы сказали, возможно, это упущение?

Итак, валкер хорош для того, чтобы убедиться, что пункт меню, помеченный как "защищенный", не отображается. Но как создать пункт меню, который помечен как "защищенный" или, что еще лучше, как принадлежащий определенной роли или capability? Похоже, мы расширяем пункты меню и добавляем больше пользовательских параметров в форму пункта меню...?

@TomAuger Добавьте соответствующие метаданные к связанному объекту записи, например, пользовательскую таксономию или поле метаданных записи. Проверяйте значение поля в валкере.

Спасибо за уточнение. Безусловно, пользовательская таксономия или произвольное поле были бы правильным решением. Другой вариант, который я придумал, основан на шаблоне. Мне не очень нравится это решение, потому что не стоит связывать шаблон со встроенной функциональностью таким образом, но есть логическая связь между шаблоном (который должен проверять, есть ли у пользователя доступ к контенту) и меню. Я опубликую свой код в ответе ниже для других пользователей с таким же вопросом.

Кто-то создал отличный плагин для этого без необходимости программирования. В нем даже есть флажки в интерфейсе редактора меню для выбора разрешенных ролей для каждого пункта меню.

Ответ пользователя toscho правильный, но он подходит для тех, кто знает, что делает и как это сделать. :) Я добавляю этот вариант как более простое решение для менее продвинутых пользователей. Идея заключается в том, чтобы иметь разные меню и отображать их в зависимости от роли пользователя.
Допустим, у вас есть 3 меню с названиями editor (редактор), author (автор) и default (по умолчанию):
if (current_user_can('Editor')){
//меню для роли редактора
wp_nav_menu( array('menu' => 'editor' ));
}elseif(current_user_can('Author')){
//меню для роли автора
wp_nav_menu( array('menu' => 'author' ));
}else{
//меню по умолчанию
wp_nav_menu( array('menu' => 'default' ));
}

Теперь вам придется управлять отдельным меню для каждой роли. Я бы предпочел добавить чекбокс к пунктам меню в редакторе. К сожалению, в class Walker_Nav_Menu_Edit
нет do_action()
– нет API для этого. Похоже на упущение.

Согласен, и я бы сам предпочел добавить чекбокс или простое поле для ввода названия нужной роли, но я разместил этот ответ как альтернативу вашему. Есть вариант использовать поле описания для каждого пункта и на его основе проверять роль, но тогда вы теряете возможность использовать описание. Это новый API, дайте ему время и создавайте как можно больше тикетов в треке, чтобы его доработали :)

Проблема с переопределением методов start_el и end_el заключается в том, что это управляет только отображением самого пункта меню, но не влияет на отображение его дочерних элементов. Думаю, вам нужно переопределить display_element, чтобы скрывать и дочерние элементы тоже.
Также можно использовать поле description пункта меню для хранения информации о том, нужно ли его отображать.
Этот код проверяет в описании каждого пункта меню наличие списка возможностей (capabilities), разделенных запятыми, например [capability: this_one, next_one]. Если текущий пользователь не обладает ни одной из указанных возможностей, пункт меню (и все его дочерние элементы) не отображаются. Строки можно легко удалить из описания, если вы хотите использовать его по прямому назначению.
/*
* Скрытие или отображение меню на основе возможностей пользователя
*
* Используйте поле description элемента меню. Если оно содержит строку вида [capability: xx, xx]
* пункт меню отображается только если пользователь имеет хотя бы одну из указанных возможностей.
* Если пункт меню не отображается, его дочерние элементы тоже скрываются.
*/
/* Пользовательский Walker */
class NASS_Nav_Walker extends Walker_Nav_Menu {
// переопределяем родительский метод
function display_element( $element, &$children_elements, $max_depth, $depth=0, $args, &$output ) {
// $element - это пункт меню
// проверяем его поле description, чтобы определить, нужно ли его отображать
// если да - просто вызываем родительский метод
$desc = $element->description;
if ( should_display_menu_item($desc) ) {
parent::display_element( $element, &$children_elements, $max_depth, $depth=0, $args, &$output );
} else {
return;
}
}
}
/* Нужно ли отображать пункт меню с таким описанием? */
function should_display_menu_item( $desc ) {
// сначала проверяем, содержит ли описание спецификацию возможностей вида
// [capability: список, разделенный, запятыми]
// предполагаем, что имена возможностей состоят только из строчных букв и подчеркиваний
$prefix = "\[capability:";
$postfix = "\]";
$pattern = '@' . $prefix . '([a-z_, ]+)' . $postfix . '@';
$answer = true;
if ( preg_match($pattern, $desc, $matches) ) { // если найдено совпадение
$found = $matches[1]; // часть внутри скобок в регулярном выражении
$caps = array_map('trim', explode(",", $found));
if ( count ($caps) > 0 ) { // есть хотя бы одна возможность
$answer = false;
// проверяем, есть ли у пользователя хотя бы одна из них
foreach ($caps as $cap) {
if ( current_user_can ($cap) ) $answer = true;
}
}
}
return $answer;
}

Я добавил этот ответ, несмотря на то, что вопрос был задан давно, потому что подумал, что он может быть полезен другим.

Я попробовал использовать поле описания для определения того, какие роли могут получить доступ к каким пунктам меню, и модифицировал код, который взял отсюда - Pimp my WP Menu
Моя модифицированная версия:
<?php
/***
* Menu WALKER - для ограничения видимости пунктов меню
* Код модифицирован - Trupti Bhatt (http://3sided.co.in)
* на основе оригинального кода отсюда - 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
{
/*
* Пользовательская переменная для хранения текущей роли
*/
private $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;
}
}
/*
* Проверить, является ли пользователь администратором
*/
private function isAdmin()
{
$current_role = $this->getCurrentUserRole();
if ( $current_role == "administrator" )
{
return true;
}
else
{
return false;
}
}
/*
* Получить все ограничения
*/
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;
}
/*
* Проверить доступ
*/
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;
}
}
/*
* Рендер элемента
*/
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;
/*
* Первый тест, добавляем пользовательский класс к каждому пункту меню
*/
$classes[] = 'my-custom-menu-class';
/*
* Определение пункта меню, соответствующего неопубликованной странице
*/
// -> Флаг для отображения вывода
$item_to_display = true;
$is_item_published = true;
// -> Сбор данных связанного объекта
$item_data = get_post($item->object_id);
// --> Если это страница, действуем на флаг
if ( !empty($item_data) && ($item->object == "page") )
{
$is_item_published = ( $item_data->post_status == "publish" ) ? true : false;
$item_output = "";
}
/*
* Определение и отображение по роли пользователя
**/
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 ( $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;
$item_output .= '</a>';
$item_output .= $args->after;
}
$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
}
}
/*
* Конфигурация ограничений
* 2 - ID страницы главной
**/
define("_USE_RESTRICTED_ACCESS", true);
$menu_restricted_access_array['subscriber'] = array('2');
?>
Это пока не самая чистая версия, но она работает. Надеюсь, что кто-то еще сможет хорошо ее использовать.

Я опубликую своё решение для других, кто может наткнуться на эту ветку. Я не на 100% доволен им, потому что не следует связывать шаблон с функциональностью меню (подход Toscho с использованием метаданных или пользовательской таксономии, вероятно, более правильный). Однако это быстрое и грязное решение. Я попытался уменьшить тесную связь, предоставив некоторые константы в начале functions.php, чтобы помочь будущим разработчикам поддерживать код:
// определяем пользовательский шаблон для защищённых паролем страниц, который используется для определения, защищён ли этот элемент в меню
define ('ZG_PROTECTED_PAGE_TEMPLATE', 'page-membersonly.php');
// определяем имя пользовательской capability для защищённых страниц
define ('ZG_PROTECTED_PAGE_CAPABILITY', 'view_member_pages');
Итак, шаблон защищённой страницы — это просто вариант single.php, но он будет проверять, имеет ли current_user_can(ZG_PROTECTED_PAGE_CAPABILITY), прежде чем отображать любой контент, по соображениям безопасности.
Далее я реализую пользовательский walker, как предложил Toscho. В данном случае walker чрезвычайно прост — мы переопределяем публичные методы start_el и end_el Walker_Nav_Menu, просто перехватывая их, чтобы задать вопрос: есть ли у нас доступ к просмотру пункта меню?
Правило простое: если страница не является "приватной" (что в данном случае определяется тем, что страница использует определённый шаблон страницы), она видна. Если она "приватная" и пользователь аутентифицирован в роли, которая имеет нужную пользовательскую capability, тогда она видна. В противном случае — нет.
Если у нас есть доступ, то нам просто нужно использовать встроенные методы Walker_Nav_Menu без изменений, поэтому мы вызываем родительский метод с тем же именем.
/* Пользовательский Walker для предотвращения появления защищённых паролем страниц в списке */
class HALCO_Nav_Walker extends Walker_Nav_Menu {
protected $is_private = false;
protected $page_is_visible = false;
// переопределяем родительский метод
function start_el(&$output, $item, $depth, $args) {
// относится ли этот пункт меню к странице, использующей наш защищённый шаблон?
$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);
}
}
// переопределяем родительский метод
function end_el(&$output, $item, $depth) {
if ($page_is_visible){
parent::end_el(&$output, $item, $depth);
}
}
}
