Newer
Older

Dries Buytaert
committed
// $Id$
/**
* @file
* API for the Drupal menu system.
*/
* Define the navigation menus, and route page requests to code based on URLs.
*
* 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 "page arguments" attribute of its menu item. The
* attribute must be an array. 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 callback" with an optional "access arguments" of each menu
* item is called before the page callback proceeds. If this returns TRUE,
* then access is granted; if FALSE, then access is denied. 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.
*
* Everything described so far is stored in the menu_router table. The
* menu_links table holds the visible menu links. By default these are
* derived from the same hook_menu definitions, however you are free to
* add more with menu_link_save().
/**
* Internal menu flag -- menu item is the root of the menu tree.
*/
/**
* Internal menu flag -- menu item is visible in the menu tree.
*/
/**
* Internal menu flag -- menu item is visible in the breadcrumb.
*/
/**
* Internal menu flag -- menu item links back to its parnet.
*/
define('MENU_LINKS_TO_PARENT', 0x0008);
/**
* Internal menu flag -- menu item can be modified by administrator.
*/
define('MENU_MODIFIED_BY_ADMIN', 0x0020);
/**
* Internal menu flag -- menu item was created by administrator.
*/
define('MENU_CREATED_BY_ADMIN', 0x0040);
/**
* Internal menu flag -- menu item is a local task.
*/
define('MENU_IS_LOCAL_TASK', 0x0080);
* @{
* Menu item definitions provide one of these constants, which are shortcuts for
* combinations of the above flags.
*/
* Menu type -- A "normal" menu item that's shown in menu and breadcrumbs.
*
* 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.

Gábor Hojtsy
committed
define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);
* Menu type -- A hidden, internal callback, typically used for API calls.
*
* Callbacks simply register a path so that the correct function is fired
* Menu type -- A normal menu item, hidden until enabled by an administrator.
*
* 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.

Gábor Hojtsy
committed
* Note for the value: 0x0010 was a flag which is no longer used, but this way
* the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate.

Gábor Hojtsy
committed
define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010);
* Menu type -- A task specific to the parent item, usually rendered as a tab.
*
* Local tasks are 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".
* Menu type -- The "default" local task, which is initially active.
*
* 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);
/**
* Internal menu status code -- Menu item was found.
*/
/**
* Internal menu status code -- Menu item was not found.
*/
/**
* Internal menu status code -- Menu item access is denied.
*/
/**
* Internal menu status code -- Menu item inaccessible because site is offline.
*/

Dries Buytaert
committed
define('MENU_SITE_OFFLINE', 4);
* @Name Menu tree parameters
* Menu tree
/**
* The maximum number of path elements for a menu callback
define('MENU_MAX_PARTS', 7);
* The maximum depth of a menu links tree - matches the number of p columns.
define('MENU_MAX_DEPTH', 9);
* @} End of "Menu tree parameters".
/**

Dries Buytaert
committed
* Returns the ancestors (and relevant placeholders) for any given path.
*
* For example, the ancestors of node/12345/edit are:
* - node/12345/edit
* - node/12345/%
* - node/%/edit
* - node/%/%
* - node/12345
* - node/%
* - node

Dries Buytaert
committed
*
* To generate these, we will use binary numbers. Each bit represents a
* part of the path. If the bit is 1, then it represents the original
* value while 0 means wildcard. If the path is node/12/edit/foo
* then the 1011 bitstring represents node/%/edit/foo where % means that
* any argument matches that part. We limit ourselves to using binary
* numbers that correspond the patterns of wildcards of router items that
* actually exists. This list of 'masks' is built in menu_rebuild().

Dries Buytaert
committed
*
* @param $parts
* An array of path parts, for the above example
* array('node', '12345', 'edit').
* @return
* An array which contains the ancestors and placeholders. Placeholders
* simply contain as many '%s' as the ancestors.

Dries Buytaert
committed
*/
function menu_get_ancestors($parts) {
$number_parts = count($parts);

Dries Buytaert
committed
$placeholders = array();
$ancestors = array();
$length = $number_parts - 1;
$end = (1 << $number_parts) - 1;
$masks = variable_get('menu_masks', array());
// Only examine patterns that actually exist as router items (the masks).
foreach ($masks as $i) {
if ($i > $end) {
// Only look at masks that are not longer than the path of interest.
continue;
}
elseif ($i < (1 << $length)) {
// We have exhausted the masks of a given length, so decrease the length.
--$length;
}

Dries Buytaert
committed
$current = '';
for ($j = $length; $j >= 0; $j--) {
if ($i & (1 << $j)) {
$current .= $parts[$length - $j];
}
else {
$current .= '%';
}
if ($j) {
$current .= '/';
}

Dries Buytaert
committed
$placeholders[] = "'%s'";
$ancestors[] = $current;
}

Dries Buytaert
committed
return array($ancestors, $placeholders);

Dries Buytaert
committed
* The menu system uses serialized arrays stored in the database for
* arguments. However, often these need to change according to the
* current path. This function unserializes such an array and does the
* necessary change.

Dries Buytaert
committed
* Integer values are mapped according to the $map parameter. For
* example, if unserialize($data) is array('view', 1) and $map is
* array('node', '12345') then 'view' will not be changed because
* it is not an integer, but 1 will as it is an integer. As $map[1]
* is '12345', 1 will be replaced with '12345'. So the result will
* be array('node_load', '12345').

Dries Buytaert
committed
*

Dries Buytaert
committed
* @param @data
* A serialized array.
* @param @map
* An array of potential replacements.

Dries Buytaert
committed
* @return

Dries Buytaert
committed
* The $data array unserialized and mapped.

Dries Buytaert
committed
*/

Dries Buytaert
committed
function menu_unserialize($data, $map) {
if ($data = unserialize($data)) {
foreach ($data as $k => $v) {
if (is_int($v)) {
$data[$k] = isset($map[$v]) ? $map[$v] : '';
}
}
return $data;

Dries Buytaert
committed
}

Dries Buytaert
committed
else {
return array();

Dries Buytaert
committed
}
}

Dries Buytaert
committed
/**
* Replaces the statically cached item for a given path.

Dries Buytaert
committed
*

Dries Buytaert
committed
* @param $path
* The path.
* @param $router_item
* The router item. Usually you take a router entry from menu_get_item and
* set it back either modified or to a different path. This lets you modify the
* navigation block, the page title, the breadcrumb and the page help in one
* call.

Dries Buytaert
committed
*/
function menu_set_item($path, $router_item) {
menu_get_item($path, $router_item);
}
/**
* Get a router item.
*
* @param $path
* The path, for example node/5. The function will find the corresponding
* node/% item and return that.
* @param $router_item
* Internal use only.
* @return
* The router item, an associate array corresponding to one row in the
* menu_router table. The value of key map holds the loaded objects. The
* value of key access is TRUE if the current user can access this page.
* The values for key title, page_arguments, access_arguments will be
* filled in based on the database values and the objects loaded.
*/
function menu_get_item($path = NULL, $router_item = NULL) {

Dries Buytaert
committed
static $router_items;

Dries Buytaert
committed
if (!isset($path)) {
$path = $_GET['q'];
}
if (isset($router_item)) {
$router_items[$path] = $router_item;
}

Dries Buytaert
committed
if (!isset($router_items[$path])) {
$original_map = arg(NULL, $path);
$parts = array_slice($original_map, 0, MENU_MAX_PARTS);

Dries Buytaert
committed
list($ancestors, $placeholders) = menu_get_ancestors($parts);
if ($router_item = db_fetch_array(db_query_range('SELECT * FROM {menu_router} WHERE path IN (' . implode (',', $placeholders) . ') ORDER BY fit DESC', $ancestors, 0, 1))) {

Dries Buytaert
committed
$map = _menu_translate($router_item, $original_map);
if ($map === FALSE) {

Dries Buytaert
committed
$router_items[$path] = FALSE;
return FALSE;

Dries Buytaert
committed
if ($router_item['access']) {
$router_item['map'] = $map;
$router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $map), array_slice($map, $router_item['number_parts']));

Dries Buytaert
committed
$router_items[$path] = $router_item;

Dries Buytaert
committed
return $router_items[$path];
* Execute the page callback associated with the current path

Dries Buytaert
committed
function menu_execute_active_handler($path = NULL) {
if (_menu_site_is_offline()) {
return MENU_SITE_OFFLINE;
}

Gábor Hojtsy
committed
if (variable_get('menu_rebuild_needed', FALSE)) {
menu_rebuild();
}
if ($router_item = menu_get_item($path)) {
registry_load_path_files();

Dries Buytaert
committed
if ($router_item['access']) {

Dries Buytaert
committed
if (drupal_function_exists($router_item['page_callback'])) {
return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);

Dries Buytaert
committed
}
}
else {
return MENU_ACCESS_DENIED;
}

Dries Buytaert
committed
return MENU_NOT_FOUND;
}

Dries Buytaert
committed
* Loads objects into the map as defined in the $item['load_functions'].
*
* @param $item

Dries Buytaert
committed
* A menu router or menu link item
* @param $map
* An array of path arguments (ex: array('node', '5'))
* @return

Gábor Hojtsy
committed
* Returns TRUE for success, FALSE if an object cannot be loaded.
* Names of object loading functions are placed in $item['load_functions'].
* Loaded objects are placed in $map[]; keys are the same as keys in the

Gábor Hojtsy
committed
* $item['load_functions'] array.
* $item['access'] is set to FALSE if an object cannot be loaded.
function _menu_load_objects(&$item, &$map) {
if ($load_functions = $item['load_functions']) {
// If someone calls this function twice, then unserialize will fail.
if ($load_functions_unserialized = unserialize($load_functions)) {
$load_functions = $load_functions_unserialized;
}
$path_map = $map;
foreach ($load_functions as $index => $function) {
if ($function) {
$value = isset($path_map[$index]) ? $path_map[$index] : '';
if (is_array($function)) {
// Set up arguments for the load function. These were pulled from
// 'load arguments' in the hook_menu() entry, but they need
// some processing. In this case the $function is the key to the
// load_function array, and the value is the list of arguments.
list($function, $args) = each($function);
$load_functions[$index] = $function;
// Some arguments are placeholders for dynamic items to process.
foreach ($args as $i => $arg) {

Gábor Hojtsy
committed
if ($arg === '%index') {
// Pass on argument index to the load function, so multiple
// occurances of the same placeholder can be identified.
$args[$i] = $index;
}

Gábor Hojtsy
committed
if ($arg === '%map') {
// Pass on menu map by reference. The accepting function must
// also declare this as a reference if it wants to modify
// the map.
$args[$i] = &$map;
}
if (is_int($arg)) {
$args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : '';
}
}
array_unshift($args, $value);
$return = call_user_func_array($function, $args);
}
else {
$return = $function($value);
}
// If callback returned an error or there is no callback, trigger 404.
if ($return === FALSE) {

Dries Buytaert
committed
$item['access'] = FALSE;
$map = FALSE;
}
$map[$index] = $return;
}
}
$item['load_functions'] = $load_functions;
return TRUE;
}
/**
* Check access to a menu item using the access callback
*
* @param $item

Dries Buytaert
committed
* A menu router or menu link item
* @param $map
* An array of path arguments (ex: array('node', '5'))
* @return

Dries Buytaert
committed
* $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
*/
function _menu_check_access(&$item, $map) {
// Determine access callback, which will decide whether or not the current
// user has access to this path.
$callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']);
// Check for a TRUE or FALSE value.

Dries Buytaert
committed
if (is_numeric($callback)) {
$item['access'] = (bool)$callback;

Dries Buytaert
committed
$arguments = menu_unserialize($item['access_arguments'], $map);
// As call_user_func_array is quite slow and user_access is a very common
// callback, it is worth making a special case for it.
if ($callback == 'user_access') {

Dries Buytaert
committed
$item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);

Dries Buytaert
committed
$item['access'] = call_user_func_array($callback, $arguments);

Dries Buytaert
committed
/**

Gábor Hojtsy
committed
* Localize the router item title using t() or another callback.

Gábor Hojtsy
committed
* Translate the title and description to allow storage of English title
* strings in the database, yet display of them in the language required
* by the current user.
*
* @param $item
* A menu router item or a menu link item.
* @param $map
* The path as an array with objects already replaced. E.g., for path
* node/123 $map would be array('node', $node) where $node is the node
* object for node 123.
* @param $link_translate
* TRUE if we are translating a menu link item; FALSE if we are
* translating a menu router item.

Gábor Hojtsy
committed
* @return
* No return value.
* $item['title'] is localized according to $item['title_callback'].
* If an item's callback is check_plain(), $item['options']['html'] becomes

Gábor Hojtsy
committed
* TRUE.
* $item['description'] is translated using t().
* When doing link translation and the $item['options']['attributes']['title']

Gábor Hojtsy
committed
* (link title attribute) matches the description, it is translated as well.

Gábor Hojtsy
committed
*/
function _menu_item_localize(&$item, $map, $link_translate = FALSE) {

Dries Buytaert
committed
$callback = $item['title_callback'];
$item['localized_options'] = $item['options'];

Gábor Hojtsy
committed
// If we are not doing link translation or if the title matches the
// link title of its router item, localize it.
if (!$link_translate || (!empty($item['title']) && ($item['title'] == $item['link_title']))) {
// t() is a special case. Since it is used very close to all the time,
// we handle it directly instead of using indirect, slower methods.
if ($callback == 't') {
if (empty($item['title_arguments'])) {
$item['title'] = t($item['title']);
}
else {
$item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
}
}

Gábor Hojtsy
committed
elseif ($callback) {
if (empty($item['title_arguments'])) {
$item['title'] = $callback($item['title']);
}
else {
$item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
}
// Avoid calling check_plain again on l() function.
if ($callback == 'check_plain') {
$item['localized_options']['html'] = TRUE;
}
}

Gábor Hojtsy
committed
elseif ($link_translate) {
$item['title'] = $item['link_title'];
}
// Translate description, see the motivation above.

Dries Buytaert
committed
if (!empty($item['description'])) {

Gábor Hojtsy
committed
$original_description = $item['description'];

Dries Buytaert
committed
$item['description'] = t($item['description']);
if ($link_translate && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) {
$item['localized_options']['attributes']['title'] = $item['description'];

Gábor Hojtsy
committed
}
}
}
/**
* Handles dynamic path translation and menu access control.
*
* When a user arrives on a page such as node/5, this function determines
* what "5" corresponds to, by inspecting the page's menu path definition,
* node/%node. This will call node_load(5) to load the corresponding node
* object.
*
* It also works in reverse, to allow the display of tabs and menu items which
* contain these dynamic arguments, translating node/%node to node/5.
*
* Translation of menu item titles and descriptions are done here to
* allow for storage of English strings in the database, and translation
* to the language required to generate the current page
*

Dries Buytaert
committed
* @param $router_item
* A menu router item
* @param $map
* An array of path arguments (ex: array('node', '5'))
* @param $to_arg

Dries Buytaert
committed
* Execute $item['to_arg_functions'] or not. Use only if you want to render a
* path from the menu table, for example tabs.
* @return
* Returns the map with objects loaded as defined in the

Dries Buytaert
committed
* $item['load_functions. $item['access'] becomes TRUE if the item is
* accessible, FALSE otherwise. $item['href'] is set according to the map.
* If an error occurs during calling the load_functions (like trying to load
* a non existing node) then this function return FALSE.
*/

Dries Buytaert
committed
function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
$path_map = $map;

Dries Buytaert
committed
if (!_menu_load_objects($router_item, $map)) {
// An error occurred loading an object.

Dries Buytaert
committed
$router_item['access'] = FALSE;
return FALSE;
}
if ($to_arg) {

Dries Buytaert
committed
_menu_link_map_translate($path_map, $router_item['to_arg_functions']);
}
// Generate the link path for the page request or local tasks.

Dries Buytaert
committed
$link_map = explode('/', $router_item['path']);
for ($i = 0; $i < $router_item['number_parts']; $i++) {
if ($link_map[$i] == '%') {
$link_map[$i] = $path_map[$i];
}
}

Dries Buytaert
committed
$router_item['href'] = implode('/', $link_map);

Gábor Hojtsy
committed
$router_item['options'] = array();

Dries Buytaert
committed
_menu_check_access($router_item, $map);
_menu_item_localize($router_item, $map);
return $map;
}
/**
* This function translates the path elements in the map using any to_arg
* helper function. These functions take an argument and return an object.
* See http://drupal.org/node/109153 for more information.
*
* @param map
* An array of path arguments (ex: array('node', '5'))
* @param $to_arg_functions
* An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
*/
function _menu_link_map_translate(&$map, $to_arg_functions) {
if ($to_arg_functions) {
$to_arg_functions = unserialize($to_arg_functions);
foreach ($to_arg_functions as $index => $function) {
// Translate place-holders into real values.
$arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
if (!empty($map[$index]) || isset($arg)) {
$map[$index] = $arg;
}
else {
unset($map[$index]);
}
}
}
}
function menu_tail_to_arg($arg, $map, $index) {
return implode('/', array_slice($map, $index));
}
/**
* This function is similar to _menu_translate() but does link-specific
* preparation such as always calling to_arg functions
*
* @param $item

Dries Buytaert
committed
* A menu link
* @return
* Returns the map of path arguments with objects loaded as defined in the

Dries Buytaert
committed
* $item['load_functions'].
* $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
* $item['href'] is generated from link_path, possibly by to_arg functions.
* $item['title'] is generated from link_title, and may be localized.
* $item['options'] is unserialized; it is also changed within the call here
* to $item['localized_options'] by _menu_item_localize().
*/
function _menu_link_translate(&$item) {
$item['options'] = unserialize($item['options']);

Dries Buytaert
committed
if ($item['external']) {
$item['access'] = 1;
$map = array();

Dries Buytaert
committed
$item['href'] = $item['link_path'];
$item['title'] = $item['link_title'];

Dries Buytaert
committed
$item['localized_options'] = $item['options'];
}
else {

Dries Buytaert
committed
$map = explode('/', $item['link_path']);
_menu_link_map_translate($map, $item['to_arg_functions']);
$item['href'] = implode('/', $map);
// Note - skip callbacks without real values for their arguments.

Dries Buytaert
committed
if (strpos($item['href'], '%') !== FALSE) {
$item['access'] = FALSE;
return FALSE;
}

Dries Buytaert
committed
// menu_tree_check_access() may set this ahead of time for links to nodes.

Dries Buytaert
committed
if (!isset($item['access'])) {
if (!_menu_load_objects($item, $map)) {

Gábor Hojtsy
committed
// An error occurred loading an object.

Dries Buytaert
committed
$item['access'] = FALSE;
return FALSE;
}
_menu_check_access($item, $map);
}

Gábor Hojtsy
committed
_menu_item_localize($item, $map, TRUE);

Dries Buytaert
committed

Dries Buytaert
committed
// Allow other customizations - e.g. adding a page-specific query string to the
// options array. For performance reasons we only invoke this hook if the link
// has the 'alter' flag set in the options array.
if (!empty($item['options']['alter'])) {
drupal_alter('translated_menu_link', $item, $map);
}

Dries Buytaert
committed
return $map;
/**
* Get a loaded object from a router item.
*
* menu_get_object() will provide you the current node on paths like node/5,
* node/5/revisions/48 etc. menu_get_object('user') will give you the user
* account on user/5 etc. Note - this function should never be called within a
* _to_arg function (like user_current_to_arg()) since this may result in an
* infinite recursion.
*
* @param $type
* Type of the object. These appear in hook_menu definitons as %type. Core
* provides aggregator_feed, aggregator_category, contact, filter_format,
* forum_term, menu, menu_link, node, taxonomy_vocabulary, user. See the
* relevant {$type}_load function for more on each. Defaults to node.
* @param $position
* The expected position for $type object. For node/%node this is 1, for
* comment/reply/%node this is 2. Defaults to 1.
* @param $path
* See menu_get_item() for more on this. Defaults to the current path.
*/
function menu_get_object($type = 'node', $position = 1, $path = NULL) {
$router_item = menu_get_item($path);
if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type . '_load') {
return $router_item['map'][$position];
}
}

Dries Buytaert
committed
* Render a menu tree based on the current path.
*
* The tree is expanded based on the current path and dynamic paths are also
* changed according to the defined to_arg functions (for example the 'My account'
* link is changed from user/% to a link with the current user's uid).
*
* @param $menu_name
* The name of the menu.
* @return
* The rendered HTML of that menu on the current page.
*/
function menu_tree($menu_name = 'navigation') {
static $menu_output = array();
if (!isset($menu_output[$menu_name])) {

Dries Buytaert
committed
$tree = menu_tree_page_data($menu_name);
$menu_output[$menu_name] = menu_tree_output($tree);
}
return $menu_output[$menu_name];
}

Dries Buytaert
committed
* Returns a rendered menu tree.
*
* @param $tree
* A data structure representing the tree as returned from menu_tree_data.
* @return
* The rendered HTML of that data structure.
function menu_tree_output($tree) {
$output = '';

Gábor Hojtsy
committed
$items = array();
// Pull out just the menu items we are going to render so that we

Gábor Hojtsy
committed
// get an accurate count for the first/last classes.
foreach ($tree as $data) {

Dries Buytaert
committed
if (!$data['link']['hidden']) {

Gábor Hojtsy
committed
$items[] = $data;
}
}

Gábor Hojtsy
committed
$num_items = count($items);
foreach ($items as $i => $data) {
$extra_class = NULL;
if ($i == 0) {
$extra_class = 'first';
}
if ($i == $num_items - 1) {
$extra_class = 'last';
}
$link = theme('menu_item_link', $data['link']);
if ($data['below']) {
$output .= theme('menu_item', $link, $data['link']['has_children'], menu_tree_output($data['below']), $data['link']['in_active_trail'], $extra_class);
}
else {
$output .= theme('menu_item', $link, $data['link']['has_children'], '', $data['link']['in_active_trail'], $extra_class);
}
return $output ? theme('menu_tree', $output) : '';
}

Dries Buytaert
committed
/**

Dries Buytaert
committed
* Get the data structure representing a named menu tree.
*
* Since this can be the full tree including hidden items, the data returned
* may be used for generating an an admin interface or a select.

Dries Buytaert
committed
*
* @param $menu_name
* The named menu links to return
* @param $item
* A fully loaded menu link, or NULL. If a link is supplied, only the
* path to root will be included in the returned tree- as if this link
* represented the current page in a visible menu.
* @return
* An tree of menu links in an array, in the order they should be rendered.
*/
function menu_tree_all_data($menu_name = 'navigation', $item = NULL) {

Dries Buytaert
committed
static $tree = array();

Dries Buytaert
committed
// Use $mlid as a flag for whether the data being loaded is for the whole tree.

Dries Buytaert
committed
$mlid = isset($item['mlid']) ? $item['mlid'] : 0;

Dries Buytaert
committed
// Generate a cache ID (cid) specific for this $menu_name and $item.
$cid = 'links:' . $menu_name . ':all-cid:' . $mlid;

Dries Buytaert
committed
if (!isset($tree[$cid])) {

Dries Buytaert
committed
// If the static variable doesn't have the data, check {cache_menu}.

Dries Buytaert
committed
$cache = cache_get($cid, 'cache_menu');
if ($cache && isset($cache->data)) {

Dries Buytaert
committed
// If the cache entry exists, it will just be the cid for the actual data.
// This avoids duplication of large amounts of data.
$cache = cache_get($cache->data, 'cache_menu');
if ($cache && isset($cache->data)) {
$data = $cache->data;
}

Dries Buytaert
committed
}

Dries Buytaert
committed
// If the tree data was not in the cache, $data will be NULL.
if (!isset($data)) {

Dries Buytaert
committed
// Build and run the query, and build the tree.

Dries Buytaert
committed
if ($mlid) {

Dries Buytaert
committed
// The tree is for a single item, so we need to match the values in its
// p columns and 0 (the top level) with the plid values of other links.
$args = array(0);
for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
$args[] = $item["p$i"];
}

Dries Buytaert
committed
$args = array_unique($args);
$placeholders = implode(', ', array_fill(0, count($args), '%d'));
$where = ' AND ml.plid IN (' . $placeholders . ')';

Dries Buytaert
committed
$parents = $args;
$parents[] = $item['mlid'];
}
else {

Dries Buytaert
committed
// Get all links in this menu.

Dries Buytaert
committed
$where = '';
$args = array();
$parents = array();
}
array_unshift($args, $menu_name);

Dries Buytaert
committed
// Select the links from the table, and recursively build the tree. We
// LEFT JOIN since there is no match in {menu_router} for an external
// link.

Dries Buytaert
committed
$data['tree'] = menu_tree_data(db_query("
SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, m.description, ml.*
FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
WHERE ml.menu_name = '%s'" . $where . "
ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC", $args), $parents);

Dries Buytaert
committed
$data['node_links'] = array();
menu_tree_collect_node_links($data['tree'], $data['node_links']);

Dries Buytaert
committed
// Cache the data, if it is not already in the cache.
$tree_cid = _menu_tree_cid($menu_name, $data);
if (!cache_get($tree_cid, 'cache_menu')) {
cache_set($tree_cid, $data, 'cache_menu');
}
// Cache the cid of the (shared) data using the menu and item-specific cid.
cache_set($cid, $tree_cid, 'cache_menu');

Dries Buytaert
committed
}

Dries Buytaert
committed
// Check access for the current user to each item in the tree.
menu_tree_check_access($data['tree'], $data['node_links']);

Dries Buytaert
committed
$tree[$cid] = $data['tree'];

Dries Buytaert
committed
}
return $tree[$cid];
}

Dries Buytaert
committed
* Get the data structure representing a named menu tree, based on the current page.
*
* The tree order is maintained by storing each parent in an individual
* field, see http://drupal.org/node/141866 for more.
*
* @param $menu_name
* The named menu links to return
* @return
* An array of menu links, in the order they should be rendered. The array
* is a list of associative arrays -- these have two keys, link and below.
* link is a menu item, ready for theming as a link. Below represents the
* submenu below the link if there is one, and it is a subtree that has the
* same structure described for the top-level array.

Dries Buytaert
committed
function menu_tree_page_data($menu_name = 'navigation') {
static $tree = array();

Dries Buytaert
committed
// Load the menu item corresponding to the current page.
if ($item = menu_get_item()) {

Dries Buytaert
committed
// Generate a cache ID (cid) specific for this page.
$cid = 'links:' . $menu_name . ':page-cid:' . $item['href'] . ':' . (int)$item['access'];

Dries Buytaert
committed
if (!isset($tree[$cid])) {

Dries Buytaert
committed
// If the static variable doesn't have the data, check {cache_menu}.

Dries Buytaert
committed
$cache = cache_get($cid, 'cache_menu');
if ($cache && isset($cache->data)) {

Dries Buytaert
committed
// If the cache entry exists, it will just be the cid for the actual data.
// This avoids duplication of large amounts of data.
$cache = cache_get($cache->data, 'cache_menu');
if ($cache && isset($cache->data)) {
$data = $cache->data;
}

Dries Buytaert
committed
// If the tree data was not in the cache, $data will be NULL.
if (!isset($data)) {

Dries Buytaert
committed
// Build and run the query, and build the tree.

Dries Buytaert
committed
if ($item['access']) {

Dries Buytaert
committed
// Check whether a menu link exists that corresponds to the current path.
$args = array($menu_name, $item['href']);
$placeholders = "'%s'";
if (drupal_is_front_page()) {
$args[] = '<front>';
$placeholders .= ", '%s'";
}
$parents = db_fetch_array(db_query("SELECT p1, p2, p3, p4, p5, p6, p7, p8 FROM {menu_links} WHERE menu_name = '%s' AND link_path IN (" . $placeholders . ")", $args));

Dries Buytaert
committed

Dries Buytaert
committed
if (empty($parents)) {

Dries Buytaert
committed
// If no link exists, we may be on a local task that's not in the links.
// TODO: Handle the case like a local task on a specific node in the menu.
$parents = db_fetch_array(db_query("SELECT p1, p2, p3, p4, p5, p6, p7, p8 FROM {menu_links} WHERE menu_name = '%s' AND link_path = '%s'", $menu_name, $item['tab_root']));

Dries Buytaert
committed
}

Dries Buytaert
committed
// We always want all the top-level links with plid == 0.

Dries Buytaert
committed
$parents[] = '0';
// Use array_values() so that the indices are numeric for array_merge().
$args = $parents = array_unique(array_values($parents));

Dries Buytaert
committed
$placeholders = implode(', ', array_fill(0, count($args), '%d'));
$expanded = variable_get('menu_expanded', array());

Dries Buytaert
committed
// Check whether the current menu has any links set to be expanded.

Dries Buytaert
committed
if (in_array($menu_name, $expanded)) {
// Collect all the links set to be expanded, and then add all of
// their children to the list as well.

Dries Buytaert
committed
do {
$result = db_query("SELECT mlid FROM {menu_links} WHERE menu_name = '%s' AND expanded = 1 AND has_children = 1 AND plid IN (" . $placeholders . ') AND mlid NOT IN (' . $placeholders . ')', array_merge(array($menu_name), $args, $args));

Dries Buytaert
committed
$num_rows = FALSE;

Dries Buytaert
committed
while ($item = db_fetch_array($result)) {
$args[] = $item['mlid'];

Dries Buytaert
committed
$num_rows = TRUE;

Dries Buytaert
committed
}
$placeholders = implode(', ', array_fill(0, count($args), '%d'));

Dries Buytaert
committed
} while ($num_rows);

Dries Buytaert
committed
}
array_unshift($args, $menu_name);
}
else {

Dries Buytaert
committed
// Show only the top-level menu items when access is denied.
$args = array($menu_name, '0');

Dries Buytaert
committed
$placeholders = '%d';
$parents = array();
}
// Select the links from the table, and recursively build the tree. We
// LEFT JOIN since there is no match in {menu_router} for an external
// link.

Dries Buytaert
committed
$data['tree'] = menu_tree_data(db_query("
SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, m.description, ml.*
FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
WHERE ml.menu_name = '%s' AND ml.plid IN (" . $placeholders . ")
ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC", $args), $parents);

Dries Buytaert
committed
$data['node_links'] = array();
menu_tree_collect_node_links($data['tree'], $data['node_links']);

Dries Buytaert
committed
// Cache the data, if it is not already in the cache.
$tree_cid = _menu_tree_cid($menu_name, $data);
if (!cache_get($tree_cid, 'cache_menu')) {
cache_set($tree_cid, $data, 'cache_menu');
}
// Cache the cid of the (shared) data using the page-specific cid.
cache_set($cid, $tree_cid, 'cache_menu');

Dries Buytaert
committed
// Check access for the current user to each item in the tree.

Dries Buytaert
committed
menu_tree_check_access($data['tree'], $data['node_links']);
$tree[$cid] = $data['tree'];

Dries Buytaert
committed
return $tree[$cid];

Dries Buytaert
committed
return array();

Dries Buytaert
committed
/**
* Helper function - compute the real cache ID for menu tree data.