Newer
Older
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
*
* The Drupal menu system drives both the navigation system from a user
* perspective and the callback system that Drupal uses to respond to URLs
* passed from the browser. For this reason, a good understanding of the
* menu system is fundamental to the creation of complex modules.
*
* Drupal's menu system follows a simple hierarchy defined by paths.
* Implementations of hook_menu() define menu items and assign them to
* paths (which should be unique). The menu system aggregates these items
* and determines the menu hierarchy from the paths. For example, if the
* paths defined were a, a/b, e, a/b/c/d, f/g, and a/b/h, the menu system
* would form the structure:
* - a
* - a/b
* - a/b/c/d
* - a/b/h
* - e
* - f/g
* Note that the number of elements in the path does not necessarily
* determine the depth of the menu item in the tree.
*
* When responding to a page request, the menu system looks to see if the
* path requested by the browser is registered as a menu item with a
* callback. If not, the system searches up the menu tree for the most
* complete match with a callback it can find. If the path a/b/i is
* requested in the tree above, the callback for a/b would be used.
*
* The found callback function is called with any arguments specified in
* the "callback arguments" attribute of its menu item. After these
* arguments, any remaining components of the path are appended as further
* arguments. In this way, the callback for a/b above could respond to a
* request for a/b/i differently than a request for a/b/j.
*
* For an illustration of this process, see page_example.module.
*
* Access to the callback functions is also protected by the menu system.
* The "access" attribute of each menu item is checked as the search for a
* callback proceeds. If this attribute is TRUE, then access is granted; if
* FALSE, then access is denied. The first found "access" attribute
* determines the accessibility of the target. Menu items may omit this
* attribute to use the value provided by an ancestor item.
*
* In the default Drupal interface, you will notice many links rendered as
* tabs. These are known in the menu system as "local tasks", and they are
* rendered as tabs by default, though other presentations are possible.
* Local tasks function just as other menu items in most respects. It is
* convention that the names of these tasks should be short verbs if
* possible. In addition, a "default" local task should be provided for
* each set. When visiting a local task's parent menu item, the default
* local task will be rendered as if it is selected; this provides for a
* normal tab user experience. This default task is special in that it
* links not to its provided path, but to its parent item's path instead.
* The default task's path is only used to place it appropriately in the
* menu hierarchy.
* Flags for use in the "type" attribute of menu items.
*/
define('MENU_IS_ROOT', 0x0001);
define('MENU_VISIBLE_IN_TREE', 0x0002);
define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004);
define('MENU_VISIBLE_IF_HAS_CHILDREN', 0x0008);
define('MENU_MODIFIABLE_BY_ADMIN', 0x0010);
define('MENU_MODIFIED_BY_ADMIN', 0x0020);
define('MENU_CREATED_BY_ADMIN', 0x0040);
define('MENU_IS_LOCAL_TASK', 0x0080);
define('MENU_LINKS_TO_PARENT', 0x0200);
/**
* @}
*/
/**
* @name Menu Item Types
* @{
* Menu item definitions provide one of these constants, which are shortcuts for
* combinations of the above flags.
*/
/**
* Normal menu items show up in the menu tree and can be moved/hidden by
* the administrator. Use this for most menu items. It is the default value if
* no menu item type is specified.
*/
define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB | MENU_MODIFIABLE_BY_ADMIN);
/**
* Item groupings are used for pages like "node/add" that simply list
* subpages to visit. They are distinguished from other pages in that they will
* disappear from the menu if no subpages exist.
*/
define('MENU_ITEM_GROUPING', MENU_VISIBLE_IF_HAS_CHILDREN | MENU_VISIBLE_IN_BREADCRUMB | MENU_MODIFIABLE_BY_ADMIN);
/**
* Callbacks simply register a path so that the correct function is fired
/**
* Dynamic menu items change frequently, and so should not be stored in the
* database for administrative customization.
*/
define('MENU_DYNAMIC_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);
* Modules may "suggest" menu items that the administrator may enable. They act
* just as callbacks do until enabled, at which time they act like normal items.
*/
define('MENU_SUGGESTED_ITEM', MENU_MODIFIABLE_BY_ADMIN);
/**
* Local tasks are rendered as tabs by default. Use this for menu items that
* describe actions to be performed on their parent item. An example is the path
* "node/52/edit", which performs the "edit" task on "node/52".
/**
* Every set of local tasks should provide one "default" task, that links to the
* same path as its parent when clicked.
*/
define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT);
* Custom items are those defined by the administrator. Reserved for internal
* use; do not return from hook_menu() implementations.
*/
define('MENU_CUSTOM_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB | MENU_CREATED_BY_ADMIN | MENU_MODIFIABLE_BY_ADMIN);
/**
* Custom menus are those defined by the administrator. Reserved for internal
* use; do not return from hook_menu() implementations.
*/
define('MENU_CUSTOM_MENU', MENU_IS_ROOT | MENU_VISIBLE_IN_TREE | MENU_CREATED_BY_ADMIN | MENU_MODIFIABLE_BY_ADMIN);
/**
* Status codes for menu callbacks.
*/
define('MENU_FOUND', 1);
define('MENU_NOT_FOUND', 2);
define('MENU_ACCESS_DENIED', 3);
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/**
* Return the menu data structure.
*
* The returned structure contains much information that is useful only
* internally in the menu system. External modules are likely to need only
* the ['visible'] element of the returned array. All menu items that are
* accessible to the current user and not hidden will be present here, so
* modules and themes can use this structure to build their own representations
* of the menu.
*
* $menu['visible'] will contain an associative array, the keys of which
* are menu IDs. The values of this array are themselves associative arrays,
* with the following key-value pairs defined:
* - 'title' - The displayed title of the menu or menu item. It will already
* have been translated by the locale system.
* - 'path' - The Drupal path to the menu item. A link to a particular item
* can thus be constructed with l($item['title'], $item['path']).
* - 'children' - A linear list of the menu ID's of this item's children.
*
* Menu ID 0 is the "root" of the menu. The children of this item are the
* menus themselves (they will have no associated path). Menu ID 1 will
* always be one of these children; it is the default "Navigation" menu.
*/
function menu_get_menu() {
global $_menu;
if (!isset($_menu['items'])) {
// _menu_build() may indirectly call this function, so prevent infinite loops.
$_menu['items'] = array();
}
return $_menu;
}
/**
* Return the local task tree.
*
* Unlike the rest of the menu structure, the local task tree cannot be cached
* nor determined too early in the page request, because the user's current
* location may be changed by a menu_set_location() call, and the tasks shown
* (just as the breadcrumb trail) need to reflect the changed location.
*/
function menu_get_local_tasks() {
global $_menu;
// Don't cache the local task tree, as it varies by location and tasks are
// allowed to be dynamically determined.
if (!isset($_menu['local tasks'])) {
// _menu_build_local_tasks() may indirectly call this function, so prevent
// infinite loops.
$_menu['local tasks'] = array();
$pid = menu_get_active_nontask_item();
if (!_menu_build_local_tasks($pid)) {
// If the build returned FALSE, the tasks need not be displayed.
$_menu['local tasks'][$pid]['children'] = array();
}
/**
* Change the current menu location of the user.
*
* Frequently, modules may want to make a page or node act as if it were
* in the menu tree somewhere, even though it was not registered in a
* hook_menu() implementation. If the administrator has rearranged the menu,
* the newly set location should respect this in the breadcrumb trail and
* expanded/collapsed status of menu items in the tree. This function
* allows this behavior.
*
* @param $location
* An array specifying a complete or partial breadcrumb trail for the
* new location, in the same format as the return value of hook_menu().
* The last element of this array should be the new location itself.
*
* This function will set the new breadcrumb trail to the passed-in value,
* but if any elements of this trail are visible in the site tree, the
* trail will be "spliced in" to the existing site navigation at that point.
*/
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
function menu_set_location($location) {
global $_menu;
$temp_id = min(array_keys($_menu['items'])) - 1;
$prev_id = 0;
foreach (array_reverse($location) as $item) {
if (isset($_menu['path index'][$item['path']])) {
$mid = $_menu['path index'][$item['path']];
if (isset ($_menu['visible'][$mid])) {
// Splice in the breadcrumb at this location.
if ($prev_id) {
$_menu['items'][$prev_id]['pid'] = $mid;
}
$prev_id = 0;
break;
}
else {
// A hidden item; show it, but only temporarily.
$_menu['items'][$mid]['type'] |= MENU_VISIBLE_IN_BREADCRUMB;
if ($prev_id) {
$_menu['items'][$prev_id]['pid'] = $mid;
}
$prev_id = $mid;
}
}
else {
$item['type'] |= MENU_VISIBLE_IN_BREADCRUMB;
if ($prev_id) {
$_menu['items'][$prev_id]['pid'] = $temp_id;
}
$_menu['items'][$temp_id] = $item;
$_menu['path index'][$item['path']] = $temp_id;
if ($prev_id) {
// Didn't find a home, so attach this to the main navigation menu.
$_menu['items'][$prev_id]['pid'] = 1;
}
$final_item = array_pop($location);
menu_set_active_item($final_item['path']);
}
/**
* Execute the handler associated with the active menu item.
*
* This is called early in the page request. The active menu item is at
* this point determined exclusively by the URL. The handler that is called
* here may, as a side effect, change the active menu item so that later
* menu functions (that display the menus and breadcrumbs, for example)
* act as if the user were in a different location on the site.
*/
function menu_execute_active_handler() {
$menu = menu_get_menu();
// Determine the menu item containing the callback.
$path = $_GET['q'];
while ($path && (!array_key_exists($path, $menu['path index']) || empty($menu['items'][$menu['path index'][$path]]['callback']))) {
if (!array_key_exists($path, $menu['path index'])) {
return MENU_NOT_FOUND;
}
if (!_menu_item_is_accessible(menu_get_active_item())) {
return MENU_ACCESS_DENIED;
}
// We found one, and are allowed to execute it.
$arg = substr($_GET['q'], strlen($menu['items'][$mid]['path']) + 1);
if (strlen($arg)) {
$arguments = array_merge($arguments, explode('/', $arg));
}
call_user_func_array($menu['items'][$mid]['callback'], $arguments);
return MENU_FOUND;
* Returns the ID of the active menu item.
static $stored_mid;
$menu = menu_get_menu();
if (is_null($stored_mid) || !empty($path)) {
$path = $_GET['q'];
$path = substr($path, 0, strrpos($path, '/'));
$stored_mid = array_key_exists($path, $menu['path index']) ? $menu['path index'][$path] : 0;
// Search for default local tasks to activate instead of this item.
$continue = TRUE;
while ($continue) {
$continue = FALSE;
if (array_key_exists('children', $menu['items'][$stored_mid])) {
foreach ($menu['items'][$stored_mid]['children'] as $cid) {
if ($menu['items'][$cid]['type'] & MENU_LINKS_TO_PARENT) {
$stored_mid = $cid;
$continue = TRUE;
}
}
}
}
return $stored_mid;
/**
* Returns the ID of the current menu item or, if the current item is a
* local task, the menu item to which this task is attached.
*/
function menu_get_active_nontask_item() {
$menu = menu_get_menu();
$mid = menu_get_active_item();
// Find the first non-task item:
$mid = $menu['items'][$mid]['pid'];
}
if ($mid) {
return $mid;
}
}
function menu_get_active_title() {
$menu = menu_get_menu();
return ucfirst($menu['items'][$mid]['title']);
}
}
function menu_get_active_help() {
if (!_menu_item_is_accessible(menu_get_active_item())) {
// Don't return help text for areas the user cannot access.
return;
}
$return = module_invoke_all('help', $path);
foreach ($return as $item) {
if (!empty($item)) {
$output .= $item ."\n";
}
/**
* Returns an array of rendered menu items in the active breadcrumb trail.
*/
function menu_get_active_breadcrumb() {
$menu = menu_get_menu();
$links[] = l(t('Home'), '');
foreach ($trail as $mid) {
if ($menu['items'][$mid]['type'] & MENU_VISIBLE_IN_BREADCRUMB) {
$links[] = theme('menu_item', $mid);
}
// The last item in the trail is the page title; don't display it here.
array_pop($links);
}
/**
* Populate the database representation of the menu.
*
* This need only be called at the start of pages that modify the menu.
*/
$menu = menu_get_menu();
$new_items = array();
foreach ($menu['items'] as $mid => $item) {
if ($mid < 0 && ($item['type'] & MENU_MODIFIABLE_BY_ADMIN)) {
$new_mid = db_next_id('menu_mid');
if (isset($new_items[$item['pid']])) {
$new_pid = $new_items[$item['pid']]['mid'];
}
else {
$new_pid = $item['pid'];
}
// Fix parent IDs for menu items already added.
if ($item['children']) {
foreach ($item['children'] as $child) {
if (isset($new_items[$child])) {
$new_items[$child]['pid'] = $new_mid;
}
}
}
$new_items[$mid] = array('mid' => $new_mid, 'pid' => $new_pid, 'path' => $item['path'], 'title' => $item['title'], 'weight' => $item['weight'], 'type' => $item['type']);
}
foreach ($new_items as $item) {
db_query('INSERT INTO {menu} (mid, pid, path, title, weight, type) VALUES (%d, %d, \'%s\', \'%s\', %d, %d)', $item['mid'], $item['pid'], $item['path'], $item['title'], $item['weight'], $item['type']);
* Returns a rendered menu tree.
$menu = menu_get_menu();
$output = '';
if (isset($menu['visible'][$pid]) && $menu['visible'][$pid]['children']) {
foreach ($menu['visible'][$pid]['children'] as $mid) {
$style = (count($menu['visible'][$mid]['children']) ? (menu_in_active_trail($mid) ? 'expanded' : 'collapsed') : 'leaf');
$output .= "<li class=\"$style\">";
$output .= theme('menu_item', $mid);
if ($all || menu_in_active_trail($mid)) {
$output .= theme('menu_tree', $mid);
$output .= "</li>\n";
}
if ($output != '') {
$output = "\n<ul>\n$output\n</ul>\n";
return $output;
/**
* Generate the HTML representing a given menu item ID.
*
* @param $mid
* The menu ID to render.
*/
function theme_menu_item($mid) {
$menu = menu_get_menu();
$link_mid = $mid;
while ($menu['items'][$link_mid]['type'] & MENU_LINKS_TO_PARENT) {
$link_mid = $menu['items'][$link_mid]['pid'];
}
return l($menu['items'][$mid]['title'], $menu['items'][$link_mid]['path']);
}
/**
* Returns the rendered local tasks. The default implementation renders
* them as tabs.
*/
function theme_menu_local_tasks() {
foreach ($local_tasks[$pid]['children'] as $mid) {
$output .= theme('menu_local_task', $mid, menu_in_active_trail($mid));
foreach ($local_tasks[$pid]['children'] as $mid) {
if (menu_in_active_trail($mid) && count($local_tasks[$mid]['children'])) {
*
* @param $mid
* The menu ID to render.
* @param $active
* Whether this tab or a subtab is the active menu item.
*/
function theme_menu_local_task($mid, $active) {
if ($active) {
return '<li class="active">'. theme('menu_item', $mid) ."</li>\n";
}
else {
return '<li>'. theme('menu_item', $mid) ."</li>\n";
}
}
/** @} End of addtogroup themeable */
/**
* Returns an array with the menu items that lead to the current menu item.
// Follow the parents up the chain to get the trail.
while ($mid && $menu['items'][$mid]) {
array_unshift($trail, $mid);
$mid = $menu['items'][$mid]['pid'];
}
}
return $trail;
}
/**
* Comparator routine for use in sorting menu items.
*/
function _menu_sort($a, $b) {
$menu = menu_get_menu();
$a = &$menu['items'][$a];
$b = &$menu['items'][$b];
return $a['weight'] < $b['weight'] ? -1 : ($a['weight'] > $b['weight'] ? 1 : ($a['title'] < $b['title'] ? -1 : 1));
}
* Build the menu by querying both modules and the database.
global $_menu;
global $user;
// Start from a clean slate.
$_menu = array();
$_menu['path index'] = array();
// Set up items array, including default "Navigation" menu.
0 => array('path' => '', 'title' => '', 'type' => MENU_IS_ROOT),
1 => array('pid' => 0, 'path' => '', 'title' => t('Navigation'), 'weight' => -50, 'access' => TRUE, 'type' => MENU_IS_ROOT | MENU_VISIBLE_IN_TREE)
);
// Build a sequential list of all menu items.
$menu_item_list = module_invoke_all('menu');
// Menu items not in the DB get temporary negative IDs.
$temp_mid = -1;
if (!array_key_exists('path', $item)) {
$item['path'] = '';
}
if (!array_key_exists('type', $item)) {
if (!array_key_exists('weight', $item)) {
$item['weight'] = 0;
}
if (!array_key_exists('callback arguments', $item)) {
$item['callback arguments'] = array();
}
$mid = $temp_mid;
// Newer menu items overwrite older ones.
unset($_menu['items'][$_menu['path index'][$item['path']]]);
}
$_menu['items'][$mid] = $item;
$_menu['path index'][$item['path']] = $mid;
$temp_mid--;
}
// Now fetch items from the DB, reassigning menu IDs as needed.
if (module_exist('menu')) {
$result = db_query('SELECT * FROM {menu}');
while ($item = db_fetch_object($result)) {
// Don't display non-custom menu items if no module declared them.
if (array_key_exists($item->path, $_menu['path index'])) {
$old_mid = $_menu['path index'][$item->path];
$_menu['items'][$item->mid] = $_menu['items'][$old_mid];
unset($_menu['items'][$old_mid]);
$_menu['path index'][$item->path] = $item->mid;
// If administrator has changed item position, reflect the change.
$_menu['items'][$item->mid]['title'] = $item->title;
$_menu['items'][$item->mid]['pid'] = $item->pid;
$_menu['items'][$item->mid]['weight'] = $item->weight;
// Next, add any custom items added by the administrator.
else if ($item->type & MENU_CREATED_BY_ADMIN) {
$_menu['items'][$item->mid] = array('pid' => $item->pid, 'path' => $item->path, 'title' => $item->title, 'access' => TRUE, 'weight' => $item->weight, 'type' => $item->type, 'callback' => '', 'callback arguments' => array());
if (!empty($item->path)) {
$_menu['path index'][$item->path] = $item->mid;
}
}
}
// Establish parent-child relationships.
foreach ($_menu['items'] as $mid => $item) {
if (!isset($item['pid'])) {
// Parent's location has not been customized, so figure it out using the path.
$parent = $item['path'];
do {
$parent = substr($parent, 0, strrpos($parent, '/'));
$pid = $parent ? $_menu['path index'][$parent] : 1;
$_menu['items'][$mid]['pid'] = $pid;
}
else {
$pid = $item['pid'];

Dries Buytaert
committed
// Don't make root a child of itself.
if ($mid) {
if (isset ($_menu['items'][$pid])) {
$_menu['items'][$pid]['children'][] = $mid;
}
else {
// If parent is missing, it is a menu item that used to be defined
// but is no longer. Default to a root-level "Navigation" menu item.
$_menu['items'][1]['children'][] = $mid;
}

Dries Buytaert
committed
}
// Prepare to display trees to the user as required.
_menu_build_visible_tree();
}
/**
* Determine whether the given menu item is accessible to the current user.
*
* Use this instead of just checking the "access" property of a menu item
* to properly handle items with fall-through semantics.
*/
function _menu_item_is_accessible($mid) {
$menu = menu_get_menu();
while ($path && (!array_key_exists($path, $menu['path index']) || !array_key_exists('access', $menu['items'][$menu['path index'][$path]]))) {
* Find all visible items in the menu tree, for ease in displaying to user.
*
* Since this is only for display, we only need title, path, and children
* for each item.
global $_menu;
if (isset($_menu['items'][$pid])) {
$parent = $_menu['items'][$pid];
$children = array();
usort($parent['children'], '_menu_sort');
foreach ($parent['children'] as $mid) {
$visible = ($parent['type'] & MENU_VISIBLE_IN_TREE) ||
($parent['type'] & MENU_VISIBLE_IF_HAS_CHILDREN && count($children) > 0);
$allowed = _menu_item_is_accessible($pid);
$_menu['visible'][$pid] = array('title' => $parent['title'], 'path' => $parent['path'], 'children' => $children);
foreach ($children as $mid) {
$_menu['visible'][$mid]['pid'] = $pid;
}
return array($pid);
}
else {
return $children;
}
}
return array();
}
/**
* Find all the items in the current local task tree.
*
* Since this is only for display, we only need title, path, and children
* for each item.
*
* At the close of this function, $_menu['local tasks'] is populated with the
* menu items in the local task tree.
*
* @return
* TRUE if the local task tree is forked. It does not need to be displayed
* otherwise.
if (isset($_menu['items'][$pid])) {
$parent = $_menu['items'][$pid];
$children = array();
if (array_key_exists('children', $parent)) {
foreach ($parent['children'] as $mid) {
if (($_menu['items'][$mid]['type'] & MENU_IS_LOCAL_TASK) && _menu_item_is_accessible($mid)) {
$children[] = $mid;
// Beware short-circuiting || operator!
$forked = _menu_build_local_tasks($mid) || $forked;
usort($children, '_menu_sort');
$forked = $forked || count($children) > 1;
$_menu['local tasks'][$pid] = array('title' => $parent['title'], 'path' => $parent['path'], 'children' => $children);
foreach ($children as $mid) {
$_menu['local tasks'][$mid]['pid'] = $pid;