I'm building a Drupal site, and have added two custom menus to give two different groups of management links (some people will see one menu or the other, some will see both, and anonymous/low-level users will see neither).
The problem is, at the moment, all users can see the menus (but the menu items are not visible).
I'm trying to create a simple permissions module - and have created the administration forms which specify which menus are viewable by which role.
But I can't find a hook which lets me override the visibility of a particular menu - only the items.
So, how do I limit access to menu by role in Drupal now that I have a list of permissions in the database?
--
I have looked at Menu per Role and Menu Access. Unfortunately, these work at the item level and not on the menus directly.
Each menu is in a block, and blocks can be set to be visible for given user group (access level).
On drupal admin site: Structure/Blocks
Menu Per Role module?
http://drupal.org/project/menu_per_role
As long you use a block as a menu you can use the access by role for block setting, provided by core.
For menu listing this function work: menu_get_names();
But it hasn't any permission checks or hooks.
Where did you want to restrict menu list? if at node editing you can alter menu there via hook_form_alter.
It's not the most elegant solution, but you can do your access check in the theme.
I've come up with a solution - instead of using the auto-generated menu blocks for display, I've created a single block and put the following code in my module:
function amh_menu_block($op = 'list', $delta = 0, $edit = array())
{
if ($op == 'list') {
$blocks[0] = array(
'info' => t('AMH Menu block'),
'weight' => 0,
'status' => 1,
'region' => 'left',
);
return $blocks;
} elseif ($op == 'view') {
switch($delta) {
case 0:
$block = array(
'subject' => '',
'content' => _amh_menu_display(),
);
break;
}
return $block;
}
}
function _amh_menu_display()
{
global $user;
$content = '';
if ($user->uid != 0) {
$result = db_query('SELECT * FROM {amh_menu_permission} p LEFT JOIN {menu_custom} m ON p.menu_name = m.menu_name LEFT JOIN {users_roles} u ON p.rid = u.rid WHERE u.uid = %d OR p.rid = 2', $user->uid);
} else {
$result = db_query('SELECT * FROM {amh_menu_permission} p LEFT JOIN {menu_custom} m ON p.menu_name = m.menu_name WHERE p.rid = 1');
}
$menus = array();
while ($m = db_fetch_object($result)) {
$menu = menu_tree($m->menu_name);
if ($menu) {
$content .= "\r\n<h2>" . $m->title . "<h2>\r\n";
$content .= theme_menu_tree($menu);
}
}
return $content;
}
This seems to work fine.
The Menu Admin Per Menu module will allow you to restrict edit access to each menu by role. https://www.drupal.org/project/menu_admin_per_menu
Related
Suppose I have parent pages A1 and B1. A1 has child pages A1.1, A1.2,A1.3 and B1 has child pages B1.1, B1.2. I want to list all the respective child pages on A1 and B1. In every child page I have an image and a title. These 2 information needs to be listed in the form of a teaser on the parent page. I need help in doing this whether by coding or by using views, I don't mind as far as I get the proper results. Thank you
You can do this is views by creating a view displaying the fields you require or a teaser. Then add a "Content Nid" contextual filter, in the configeration for this filter under "WHEN THE FILTER VALUE IS NOT AVAILABLE" select "Provide default value" and then "PHP Code" then the code I use is as follows
$children = array();
$current = db_query("select menu_name, mlid from {menu_links} where link_path = :node", array(':node' => $_GET['q']));
$current_info = array();
foreach ($current as $value) {
$current_info[] = $value;
}
if($current_info) {
$result = db_query("select mlid, plid, link_path, link_title from {menu_links} where menu_name=:menu and plid=:mlid and hidden=0 order by weight, link_title", array(':menu' => $current_info[0]->menu_name, ':mlid' => $current_info[0]->mlid));
foreach ($result as $row) {
$children[] = $row;
}
}
$nids = array();
foreach ($children as $value){
if( substr( $value->link_path, 0, 5 ) == 'node/' ){
$nids[] = substr( $value->link_path, 5 );
}
}
return implode('+',$nids);
The last thing to do, under "more" at the bottom of the page sellect "Allow multiple values"
I'm using menu_tree_all_data() to get whole menu structure and then I'm "manually" crawling menu tree.
Also just after reading the tree, I'm calling menu_tree_add_active_path() which will add active trail indicator. It's part of menu block module so you'll have to install it and don't forget to add dependencies for menu block in your module.
$tree = menu_tree_all_data($menu);
menu_tree_add_active_path($tree);
Does anyone know how or can guide me in the right direction on how to add a body css class for the current node's taxonomy term? i.e. <body class="term-dogs"> where "dogs" is the taxonomy term name. It could also be just the term ID. Either way is fine I just need a solution. This will be for a Drupal 7 zen sub-theme
This answer took longer than I expected to figure out. The hard part was collecting the terms on the node, since All taxonomy functions relating to nodes have been removed or refactored. Ultimately, page 355 of Pro Drupal 7 Development saved the day with a snippet that does the job previously handled by taxonomy_node_get_terms.
Below is the code that worked for me (look for the part that says "MAGIC BEGINS HERE"). Assuming you're creating a sub-theme of Zen, you'll want to move this to your sub-theme's template.php file and rename it to YOURSUBTHEMENAME_preprocess_html:
/**
* Override or insert variables into the html template.
*
* #param $vars
* An array of variables to pass to the theme template.
* #param $hook
* The name of the template being rendered ("html" in this case.)
*/
function zen_preprocess_html(&$vars, $hook) {
// If the user is silly and enables Zen as the theme, add some styles.
if ($GLOBALS['theme'] == 'zen') {
include_once './' . drupal_get_path('theme', 'zen') . '/zen-internals/template.zen.inc';
_zen_preprocess_html($vars, $hook);
}
// Classes for body element. Allows advanced theming based on context
// (home page, node of certain type, etc.)
if (!$vars['is_front']) {
// Add unique class for each page.
$path = drupal_get_path_alias($_GET['q']);
// Add unique class for each website section.
list($section, ) = explode('/', $path, 2);
if (arg(0) == 'node') {
if (arg(1) == 'add') {
$section = 'node-add';
}
elseif (is_numeric(arg(1)) && (arg(2) == 'edit' || arg(2) == 'delete')) {
$section = 'node-' . arg(2);
}
// MAGIC BEGINS HERE
$node = node_load(arg(1));
$results = field_view_field('node', $node, 'field_tags', array('default'));
foreach ($results as $key => $result) {
if (is_numeric($key)) {
$vars['classes_array'][] = strtolower($result['#title']);
}
}
// MAGIC ENDS HERE
}
$vars['classes_array'][] = drupal_html_class('section-' . $section);
}
if (theme_get_setting('zen_wireframes')) {
$vars['classes_array'][] = 'with-wireframes'; // Optionally add the wireframes style.
}
// Store the menu item since it has some useful information.
$vars['menu_item'] = menu_get_item();
switch ($vars['menu_item']['page_callback']) {
case 'views_page':
// Is this a Views page?
$vars['classes_array'][] = 'page-views';
break;
case 'page_manager_page_execute':
case 'page_manager_node_view':
case 'page_manager_contact_site':
// Is this a Panels page?
$vars['classes_array'][] = 'page-panels';
break;
}
}
I needed to know how to do this and Matt V's solution worked perfectly. I made a couple of additions to his work. I called drupal_html_class which replaces spaces and invalid characters. And I added in the term ID to allow you to target a term even if the name of the term changes.
// MAGIC BEGINS HERE
$node = node_load(arg(1));
$results = field_view_field('node', $node, 'field_tags', array('default'));
foreach ($results as $key => $result) {
if (is_numeric($key)) {
// Call drupal_html_class to make safe for a css class (remove spaces, invalid characters)
$vars['classes_array'][] = "taxonomy-" . strtolower(drupal_html_class( $result['#title']) );
// Add taxonomy ID. This will allow targeting of the taxonomy class even if the title changes
$vars['classes_array'][] = "taxonomy-id-" . $result['#options']['entity']->tid ;
}
}
// MAGIC ENDS HERE
Not sure what you mean with that body tag, but the classes on the node are generated here:
http://api.drupal.org/api/drupal/modules--node--node.module/function/template_preprocess_node/7
You can add more by implementing yourmodule_preprocess_node($vars) and then add whatever you want to $vars['classes_array']
I'm writing a custom module, and I would like to do some checks before the node is deleted. Is there a hook that gets trigerred before a node is deleted? And is there a way to somehow prevent the deletion? BTW, I'm using drupal6
You can use hook_menu_alter to point the menu callback node/%node/delete to your own function. Your function can do whatever checks you want and then present the node_delete_confirm form if the checks pass.
This will remove the Delete button and add your own button and action. This will not prevent users from using the URL /node/[nid]/delete to delete the node, use the permission settings for that.
function my_module_form_alter(&$form, &$form_state, $form_id) {
if($form_id == "allocation_node_form") {
if (isset($form['#node']->nid)) {
$form['buttons']['my_remove'] = array(
'#type' => 'submit',
'#value' => 'Remove',
'#weight' => 15,
'#submit' => array('allocation_remove_submit'),
);
if($user->uid != 1) {
unset($form['buttons']['delete']);
$form['buttons']['#suffix'] = "<br>".t("<b>Remove</b> will...");
}else{
$form['buttons']['#suffix'] = t("<b>Delete</b> only if ...");
}
}
}
}
function allocation_remove_submit($form, &$form_state) {
if (is_numeric($form_state['values']['field_a_team'][0]['nid'])) {
//my actions
//Clear forms cache
$cid = 'content:'. $form_state['values']['nid'].':'. $form_state['values']['vid'];
cache_clear_all($cid, 'cache_content', TRUE);
//Redirect
drupal_goto("node/".$form_state['values']['field_a_team'][0]['nid']);
}else{
drupal_set_message(t("Need all values to be set"), "warning");
}
}
You can use hook_nodeapi op delete.
It might be a bad idea trying to stop the deletion of a node, since you don't know what other modules have done, like deleting cck field values etc.
There is no hook you can use to do actions before a node is being deleted. The above is the closest you can come.
Use form_alter and remove the delete button if your conditions are met.
Something like this.
function xxx_contact_form_alter(&$form, $form_state, $form_id) {
global $user;
if (strstr($form_id, 'xxx_node_form')) {
// Stop deletion of xxx users unless you are an admin
if (($form['#node']->uid) == 0 && ($user->uid != 1)) {
unset($form['actions']['delete']);
}
}
}
This custom module code is for Drupal 7, but I'm sure a similar concept applies to Drupal 6. Plus, by now, you're most probably looking for a solution for Drupal 7.
This code will run "before" a node is deleted and hence you can run the checks you want and then optionally hide the delete button to prevent the node from being deleted. Check the function's comments for more info.
This is a screenshot showcasing the end result:
And this is the custom code used:
<?php
/**
* Implements hook_form_FORM_ID_alter() to conditionally prevent node deletion.
*
* We check if the current node has child menu items and, if yes, we prevent
* this node's deletion and also show a message explaining the situation and
* links to the child nodes so that the user can easily delete them first
* or move them to another parent menu item.
*
* This can be useful in many cases especially if you count on the paths of
* the child items being derived from their parent item path, for example.
*/
function sk_form_node_delete_confirm_alter(&$form, $form_state) {
//Check if we have a node id and stop if not
if(empty($form['nid']['#value'])) {
return;
}
//Load the node from the form
$node = node_load($form['nid']['#value']);
//Check if node properly loaded and stop if not
//Empty checks for both $node being not empty and also for its property nid
if(empty($node->nid)) {
return;
}
//Get child menu items array for this node
$children_nids = sk_get_all_menu_node_children_ids('node/' . $node->nid);
$children_count = count($children_nids);
//If we have children, do set a warning and disable delete button and such
//so that this node cannot be deleted by the user.
//Note: we are not 100% that this prevents the user from deleting it through
//views bulk operations for example or by faking a post request, but for our
//needs, this is adequate as we trust the editors on our websites.
if(!empty($children_nids)) {
//Construct explanatory message
$msg = '';
$t1 = '';
$t1 .= '%title is part of a menu and has %count child menu items. ';
$t1 .= 'If you delete it, the URL paths of its children will no longer work.';
$msg .= '<p>';
$msg .= t($t1, array('%title' => $node->title, '%count' => $children_count));
$msg .= '</p>';
$t2 = 'Please check the %count child menu items below and delete them first.';
$msg .= '<p>';
$msg .= t($t2, array('%count' => $children_count));
$msg .= '</p>';
$msg .= '<ol>';
$children_nodes = node_load_multiple($children_nids);
if(!empty($children_nodes)) {
foreach($children_nodes as $child_node) {
if(!empty($child_node->nid)) {
$msg .= '<li>';
$msg .= '<a href="' . url('node/' . $child_node->nid) . '">';
$msg .= $child_node->title;
$msg .= '</a>';
$msg .= '</li>';
}
}
}
$msg .= '</ol>';
//Set explanatory message
$form['sk_children_exist_warning'] = array(
'#markup' => $msg,
'#weight' => -10,
);
//Remove the 'This action cannot be undone' message
unset($form['description']);
//Remove the delete button
unset($form['actions']['submit']);
}
}
For more info, check this detailed blog post about conditionally preventing node deletion in Drupal 7. It has details about the whole process and also links to resources including how to easily create a custom module where you can copy/paste the above code into to get it working.
Good luck.
There isn't a hook that gets called before the node gets deleted, but Drupal does check with node_access to see if the user is allowed to delete the node before continuing with the deletion.
You could set the node access permissions to not allow the user to delete the node: it won't help if the user is user 1 or has the administer nodes permission, so don't give those permissions to untrusted users (i.e. people who would delete a node). This is also the Drupal Way to prevent unwarranted node deletions.
you can use hook_access and put conditions if op == delete. if you conditions fullfilled return True otherwise return false. in case of false your node will not be deleted.
Remember for admin this will not be triggered.
I really only need the mlid and title text for the first level below a certain menu item. Here's what I'm doing at the moment. (It works, but I suspect there may be a more drupal-y way.):
/**
* Get all the children menu items below 'Style Guide' and put them in this format:
* $menu_items[mlid] = 'menu-title'
* #return array
*/
function mymod_get_menu_items() {
$tree = menu_tree_all_data('primary-links');
$branches = $tree['49952 Parent Item 579']['below']; // had to dig for that ugly key
$menu_items = array();
foreach ($branches as $menu_item) {
$menu_items[$menu_item['link']['mlid']] = $menu_item['link']['title'];
}
return $menu_items;
}
Is there?
Actually there is an easy way to get that information by using menu_build_tree():
// Set $path to the internal Drupal path of the parent or
// to NULL for the current path
$path = 'node/123';
$parent = menu_link_get_preferred($path);
$parameters = array(
'active_trail' => array($parent['plid']),
'only_active_trail' => FALSE,
'min_depth' => $parent['depth']+1,
'max_depth' => $parent['depth']+1,
'conditions' => array('plid' => $parent['mlid']),
);
$children = menu_build_tree($parent['menu_name'], $parameters);
$children contains all information you need. menu_build_tree() checks access or translation related restrictions too so you only get what the user really should see.
afaik, there isn't (i hope i am wrong).
for the while, instead of digging for ugly keys, you can turn your function into a more abstract helper function by simply adding a foreach ($tree). then you can use your own logic to output what you want (mlid, in this case). here is my suggestion:
/**
* Get the children of a menu item in a given menu.
*
* #param string $title
* The title of the parent menu item.
* #param string $menu
* The internal menu name.
*
* #return array
* The children of the given parent.
*/
function MY_MODULE_submenu_tree_all_data($title, $menu = 'primary-links') {
$tree = menu_tree_all_data($menu);
foreach ($tree as $branch) {
if ($branch['link']['title'] == $title) {
return $branch['below'];
}
}
return array();
}
Have you looked into the Menu block module? Some more details about this module (from its project page):
... have you ever used the Main and Secondary menu links feature on your theme and wondered “how the hell do I display any menu items deeper than that?”
Well, that’s what this module does. It provides configurable blocks of menu trees starting with any level of any menu. And more!
So if you’re only using your theme’s Main menu links feature, you can add and configure a “Main menu (levels 2+)” block. That block would appear once you were on one of the Main menu’s pages and would show the menu tree for the 2nd level (and deeper) of your Main menu and would expand as you traversed down the tree. You can also limit the depth of the menu’s tree (e.g. “Main menu (levels 2-3)”) and/or expand all the child sub-menus (e.g. “Main menu (expanded levels 2+)”).
I use this :
Just add your path and eventualy the menu and it will give you the child.
function MY_MODULE_submenu_tree_all_data($path, $menu = 'main-menu', $curr_level = 0, $rebuilt_path='', $childtree = array()) {
$tree = menu_tree_all_data($menu);
$args = explode('/', $path);
$rebuilt_path = empty($rebuilt_path) ? $args[$curr_level] : $rebuilt_path . '/' . $args[$curr_level];
foreach ($tree as $branch) {
if ($branch['link']['link_path'] == $rebuilt_path) {
$childtree = $branch['below'];
if ($rebuilt_path != $path) {
$curr_level++;
MY_MODULE_submenu_tree_all_data($path, $menu, $curr_level, $rebuilt_path, $childtree);
}
}
}
$items = array();
foreach ($childtree as $child) {
$items[] = l($child['link']['title'], $child['link']['link_path']);
}
return theme('item_list', array('items' => $items, 'attributes' => array(), 'type' => 'ul'));
}
Here's a helper function to return a whole subtree of a menu, starting at a specified mlid. Some of the other posts only return the direct descendants of the current item; this will return ALL descendants.
By default it gives you the subtree starting with the current page, but you can pass in any menu tree (as returned by menu_build_tree) and any mlid.
function _menu_build_subtree($menu=NULL,$mlid=NULL) {
if ($menu == NULL || $mlid == NULL) {
$parent = menu_link_get_preferred();
}
$menu = !is_null($menu) ? $menu : menu_build_tree($parent['menu_name']);
$mlid = !is_null($mlid) ? $mlid : $parent['mlid'];
foreach ($menu as $branch) {
if ($branch['link']['mlid'] == $mlid) {
return $branch;
}
$twig = _menu_build_subtree($branch['below'],$mlid);
if ($twig) { return $twig; }
}
return array();
}
Under Pages menu in Wordpress Admin page, I got this layout:
Pages
Edit (url: edit-pages.php)
Add New (url: page-new.php)
Special Pages (url: edit-pages.php?special-pages=true)
as you can see, I've added a new submenu item called Special Pages which is pretty much a link to to Edit page with custom filter. Because Wordpress use file name to identify and highlight the submenu item, so whenever I click on Special Pages, the Edit submenu item is selected. Is there anyway to force Wordpress to select Special Pages menu item instead?
Cheers
better solution:
add_filter('parent_file', 'my_plugin_select_submenu');
function my_plugin_select_submenu($file) {
global $plugin_page;
if ('__my-current-submenu-slug__' == $plugin_page) {
$plugin_page = '__my-submenu-slug-to-select__';
}
return $file;
}
To further clarify Ken Vu's answer, edit the global variables $submenu_file and $parent_file. E.g., to highlight your page:
global $submenu_file;
$submenu_file = "edit-pages.php?special-pages=true";
If you need to change the top-level item highlighted, use $parent_file. E.g., highlight the "Writing" setting page:
global $parent_file;
global $submenu_file;
$parent_file = 'options-general.php';
$submenu_file = 'options-writing.php';
Solution: use $submenu_file variable
$submenu_file = "edit-pages.php?special-pages=true"
Thanks Ken Vu and Jonathan Brinley. Using your answers, I finally got the highlighting of my admin menu to work properly. As I struggled a bit to get it to work, I though I would post the entire result here, so other people can find it more easily :
The idea is to call the parent_file filter (undocumented, as many Wordpress parts unfornatunately). In my case, I was adding a custom menu instead of the default generated when creating a custom post type.
In my custom post code, I call the add_meta_boxes action. Within this hook, I issue my call to the parent_file filter :
add_filter('parent_file', array(&$this, 'highlight_admin_menu'));
_
Then this is how my hightlight_admin_menu function looks like :
function highlight_admin_menu($some_slug){
global $parent_file;
$parent_file = 'post.php?post=149&action=edit';
return $parent_file;
}
_
This got my menu to highlight properly. Try playing around with you own code to know where to issue the add_filter('parent_file', ...) code. Find a bit of code executed only on that particular page load, but soon enough that it is still possible to modify the $parent_file variable.
I hope this helps!
For changing the highlighted menu item for a submenu item, the proper filter is submenu_file.
add_filter('submenu_file', 'menuBold');
static function menuBold($submenu_file)
{
if ( checkProperPage($_GET) ) {
// The address of the link to be highlighted
return 'post-new?post_type=foobar&foo=bar';
}
// Don't change anything
return $submenu_file;
}
The check happens in WP's ~/wp-admin/menu-header.php file on line 194 (Wordpress 4.5.3):
if ( isset( $submenu_file ) ) {
if ( $submenu_file == $sub_item[2] )
$class[] = 'current';
...
}
You can modify this code to work for you. You can change both parent and submenu with that. Tested code.
function change_active_parent($submenu_file)
{
global $parent_file;
$zone = 'edit-tags.php?taxonomy=zone&post_type=product';
$storefront = 'edit-tags.php?taxonomy=storefront&post_type=product';
$container = 'edit-tags.php?taxonomy=container&post_type=product';
if (esc_html($zone) == $submenu_file) {
$parent_file = 'parent';
$submenu_file = $zone;
}
elseif (esc_html($storefront) == $submenu_file) {
$parent_file = 'parent';
$submenu_file = $storefront;
}
elseif (esc_html($container) == $submenu_file) {
$parent_file = 'parent';
$submenu_file = $container;
}
return $submenu_file;
}
add_filter( 'submenu_file', 'change_active_parent' );
Use the load-{$page_hook} action hook and modify the necessary globals:
/**
* For giggles, lets make an admin page that is not "in the menu" to play with.
*/
add_action('admin_menu', 'mort1305_admin_menu');
function mort1305_admin_menu() {
add_submenu_page(
NULL,
'Page Title',
'',
'administrator',
'my_slug',
'mort1305_page_content'
);
}
/**
* The menu item to highlight and the submenu item to embolden.
*/
add_action('load-admin_page_my_slug', 'mort1305_on_page_load');
function mort1305_on_page_load(){
global $plugin_file, $submenu_file, $title;
$plugin_page = 'slug-of-menu-item-to-be-highlighted';
$submenu_file = 'slug-of-submenu-item-to-be-bold';
foreach($submenu[NULL] as $submenu_arr) {
if($submenu_arr[2] === 'test_page_slug') {
$title = $submenu_arr[3];
break;
}
}
}
/**
* Page content to display.
*/
function mort_1305_page_content() {
echo This is the '. get_admin_page_title() .' page. The slug of my parent is '. get_admin_page_parent() .'.';
}