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 parent.
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);

Angie Byron
committed
/**
* Internal menu flag -- menu item is a local action.
*/
define('MENU_IS_LOCAL_ACTION', 0x0100);
* @{
* 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".
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_VISIBLE_IN_BREADCRUMB);
* 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 | MENU_VISIBLE_IN_BREADCRUMB);

Angie Byron
committed
/**
* Menu type -- An action specific to the parent, usually rendered as a link.
*
* Local actions are menu items that describe actions on the parent item such
* as adding a new user, taxonomy term, etc.
*/
define('MENU_LOCAL_ACTION', MENU_IS_LOCAL_TASK | MENU_IS_LOCAL_ACTION | MENU_VISIBLE_IN_BREADCRUMB);

Angie Byron
committed

Angie Byron
committed
/**
* @name Menu context types
* @{
* Flags for use in the "context" attribute of menu router items.
*/

Dries Buytaert
committed
/**
* Internal menu flag: Invisible local task.
*
* This flag may be used for local tasks like "Delete", so custom modules and
* themes can alter the default context and expose the task by altering menu.
*/
define('MENU_CONTEXT_NONE', 0x0000);

Angie Byron
committed
/**
* Internal menu flag: Local task should be displayed in page context.
*/
define('MENU_CONTEXT_PAGE', 0x0001);
/**
* Internal menu flag: Local task should be displayed inline.
*/
define('MENU_CONTEXT_INLINE', 0x0002);
/**
* @} End of "Menu context types".
*/
/**
* 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);

Angie Byron
committed
/**
* Internal menu status code -- Everything is working fine.
*/
define('MENU_SITE_ONLINE', 5);
* @Name Menu tree parameters
* Menu tree
/**
* The maximum number of path elements for a menu callback
define('MENU_MAX_PARTS', 9);
* 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

Dries Buytaert
committed
* any argument matches that part. We limit ourselves to using binary
* numbers that correspond the patterns of wildcards of router items that

Dries Buytaert
committed
* 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').

Dries Buytaert
committed
*

Dries Buytaert
committed
* @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
$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
$ancestors[] = $current;
}

Dries Buytaert
committed
return $ancestors;

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
*

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.

Dries Buytaert
committed
*
* @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.

Dries Buytaert
committed
* The values for key title, page_arguments, access_arguments, and
* theme_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
$router_items = &drupal_static(__FUNCTION__);

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
$ancestors = menu_get_ancestors($parts);

Angie Byron
committed

Dries Buytaert
committed
// Since there is no limit to the length of $path, use a hash to keep it
// short yet unique.
$cid = 'menu_item:' . hash('sha256', $path);

Angie Byron
committed
if ($cached = cache_get($cid, 'cache_menu')) {
$router_item = $cached->data;
}
else {
$router_item = db_query_range('SELECT * FROM {menu_router} WHERE path IN (:ancestors) ORDER BY fit DESC', 0, 1, array(':ancestors' => $ancestors))->fetchAssoc();
cache_set($cid, $router_item, 'cache_menu');
}

Dries Buytaert
committed
if ($router_item) {

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

Angie Byron
committed
$router_item['original_map'] = $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_item['theme_arguments'] = array_merge(menu_unserialize($router_item['theme_arguments'], $map), array_slice($map, $router_item['number_parts']));

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

Dries Buytaert
committed
return $router_items[$path];

Dries Buytaert
committed
* Execute the page callback associated with the current path.

Dries Buytaert
committed
* @param $path
* The drupal path whose handler is to be be executed. If set to NULL, then
* the current path is used.
* @param $deliver
* (optional) A boolean to indicate whether the content should be sent to the
* browser using the appropriate delivery callback (TRUE) or whether to return
* the result to the caller (FALSE).
*/
function menu_execute_active_handler($path = NULL, $deliver = TRUE) {

Angie Byron
committed
// Check if site is offline.
$page_callback_result = _menu_site_is_offline() ? MENU_SITE_OFFLINE : MENU_SITE_ONLINE;
// Allow other modules to change the site status but not the path because that
// would not change the global variable. hook_url_inbound_alter() can be used
// to change the path. Code later will not use the $read_only_path variable.
$read_only_path = !empty($path) ? $path : $_GET['q'];
drupal_alter('menu_site_status', $page_callback_result, $read_only_path);
// Only continue if the site status is not set.
if ($page_callback_result == MENU_SITE_ONLINE) {

Dries Buytaert
committed
// Rebuild if we know it's needed, or if the menu masks are missing which
// occurs rarely, likely due to a race condition of multiple rebuilds.
if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) {
menu_rebuild();
}
if ($router_item = menu_get_item($path)) {
if ($router_item['access']) {

Dries Buytaert
committed
if ($router_item['include_file']) {
require_once DRUPAL_ROOT . '/' . $router_item['include_file'];

Dries Buytaert
committed
}
$page_callback_result = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
}
else {
$page_callback_result = MENU_ACCESS_DENIED;

Dries Buytaert
committed
}
}
else {

Dries Buytaert
committed
$page_callback_result = MENU_NOT_FOUND;

Dries Buytaert
committed
}

Dries Buytaert
committed
// Deliver the result of the page callback to the browser, or if requested,
// return it raw, so calling code can do more processing.
if ($deliver) {
$default_delivery_callback = (isset($router_item) && $router_item) ? $router_item['delivery_callback'] : NULL;
drupal_deliver_page($page_callback_result, $default_delivery_callback);
}
else {
return $page_callback_result;
}

Dries Buytaert
committed
}

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'))

Dries Buytaert
committed
*

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
// occurrences 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'))

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]);
elseif (function_exists($callback)) {

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.

Dries Buytaert
committed
*

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'];

Dries Buytaert
committed
// All 'class' attributes are assumed to be an array during rendering, but
// links stored in the database may use an old string value.
// @todo In order to remove this code we need to implement a database update
// including unserializing all existing link options and running this code
// on them, as well as adding validation to menu_link_save().
if (isset($item['options']['attributes']['class']) && is_string($item['options']['attributes']['class'])) {
$item['localized_options']['attributes']['class'] = explode(' ', $item['options']['attributes']['class']);
}
// If we are translating the title of a menu link, and its title is the same
// as the corresponding router item, then we can use the title information
// from the router. If it's customized, then we need to use the link title
// itself; can't localize.
// If we are translating a router item (tabs, page, breadcrumb), then we
// can always use the information from the router item.
if (!$link_translate || ($item['title'] == $item['link_title'])) {

Gábor Hojtsy
committed
// 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));
}
}
elseif ($callback && function_exists($callback)) {

Gábor Hojtsy
committed
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

Dries Buytaert
committed
* 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.

Dries Buytaert
committed
*
* @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

Dries Buytaert
committed
* 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) {

Angie Byron
committed
if ($to_arg && !empty($router_item['to_arg_functions'])) {

Dries Buytaert
committed
// Fill in missing path elements, such as the current uid.
_menu_link_map_translate($map, $router_item['to_arg_functions']);
}
// The $path_map saves the pieces of the path as strings, while elements in
// $map may be replaced with loaded objects.
$path_map = $map;

Angie Byron
committed
if (!empty($router_item['load_functions']) && !_menu_load_objects($router_item, $map)) {
// An error occurred loading an object.

Dries Buytaert
committed
$router_item['access'] = FALSE;
return FALSE;
}
// 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);

Dries Buytaert
committed
// For performance, don't localize an item the user can't access.
if ($router_item['access']) {
_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.
*
* 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) {

Angie Byron
committed
$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

Dries Buytaert
committed
*
* @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']);

Angie Byron
committed
if (!empty($item['to_arg_functions'])) {
_menu_link_map_translate($map, $item['to_arg_functions']);
}

Dries Buytaert
committed
$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'])) {

Angie Byron
committed
if (!empty($item['load_functions']) && !_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);
}
// For performance, don't localize a link the user can't access.
if ($item['access']) {
_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.
*

Dries Buytaert
committed
* menu_get_object() provides access to objects loaded by the current router
* item. For example, on the page node/%node, the router loads the %node object,
* and calling menu_get_object() will return that. Normally, it is necessary to
* specify the type of object referenced, however node is the default.
* The following example tests to see whether the node being displayed is of the
* "story" content type:
* @code
* $node = menu_get_object();
* $story = $node->type == 'story';
* @endcode
*
* @param $type
* Type of the object. These appear in hook_menu definitions 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

Dries Buytaert
committed
* The position of the object in the path, where the first path segment is 0.
* For node/%node, the position of %node is 1, but for comment/reply/%node,
* it's 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

Angie Byron
committed
* changed according to the defined to_arg functions (for example the 'My

Dries Buytaert
committed
* account' link is changed from user/% to a link with the current user's uid).
*
* @param $menu_name
* The name of the menu.

Dries Buytaert
committed
*
* @return
* The rendered HTML of that menu on the current page.
*/

Dries Buytaert
committed
function menu_tree($menu_name) {

Dries Buytaert
committed
$menu_output = &drupal_static(__FUNCTION__, 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.

Dries Buytaert
committed
* The menu item's LI element is given one of the following classes:
* - expanded: The menu item is showing its submenu.
* - collapsed: The menu item has a submenu which is not shown.
* - leaf: The menu item has no submenu.
*
* @param $tree
* A data structure representing the tree as returned from menu_tree_data.

Dries Buytaert
committed
*

Dries Buytaert
committed
* A structured array to be rendered by drupal_render().
function menu_tree_output($tree) {

Dries Buytaert
committed
$build = array();

Gábor Hojtsy
committed
$items = array();

Dries Buytaert
committed
// Pull out just the menu links 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) {

Dries Buytaert
committed
$class = array();

Gábor Hojtsy
committed
if ($i == 0) {

Dries Buytaert
committed
$class[] = 'first';

Gábor Hojtsy
committed
}
if ($i == $num_items - 1) {

Dries Buytaert
committed
$class[] = 'last';

Gábor Hojtsy
committed
}

Dries Buytaert
committed
// Set a class for the <li>-tag. Since $data['below'] may contain local
// tasks, only set 'expanded' class if the link also has children within
// the current menu.
if ($data['link']['has_children'] && $data['below']) {

Dries Buytaert
committed
$class[] = 'expanded';
}
elseif ($data['link']['has_children']) {
$class[] = 'collapsed';

Gábor Hojtsy
committed
}
else {

Dries Buytaert
committed
$class[] = 'leaf';

Dries Buytaert
committed
// Set a class if the link is in the active trail.
if ($data['link']['in_active_trail']) {
$class[] = 'active-trail';
$data['localized_options']['attributes']['class'][] = 'active-trail';
}
// Allow menu-specific theme overrides.
$element['#theme'] = 'menu_link__' . $data['link']['menu_name'];

Dries Buytaert
committed
$element['#attributes']['class'] = $class;
$element['#title'] = $data['link']['title'];
$element['#href'] = $data['link']['href'];

Angie Byron
committed
$element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array();

Dries Buytaert
committed
$element['#below'] = $data['below'] ? menu_tree_output($data['below']) : $data['below'];
$element['#original_link'] = $data['link'];
// Index using the link's unique mlid.
$build[$data['link']['mlid']] = $element;

Dries Buytaert
committed
if ($build) {
// Make sure drupal_render() does not re-order the links.
$build['#sorted'] = TRUE;
// Add the theme wrapper for outer markup.
// Allow menu-specific theme overrides.
$build['#theme_wrappers'][] = 'menu_tree__' . $data['link']['menu_name'];

Dries Buytaert
committed
}
return $build;

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

Angie Byron
committed
* @param $link

Dries Buytaert
committed
* A fully loaded menu link, or NULL. If a link is supplied, only the

Angie Byron
committed
* path to root will be included in the returned tree - as if this link

Dries Buytaert
committed
* represented the current page in a visible menu.

Angie Byron
committed
* @param $max_depth
* Optional maximum depth of links to retrieve. Typically useful if only one
* or two levels of a sub tree are needed in conjunction with a non-NULL
* $link, in which case $max_depth should be greater than $link['depth'].
*

Dries Buytaert
committed
* @return
* An tree of menu links in an array, in the order they should be rendered.
*/

Angie Byron
committed
function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) {

Dries Buytaert
committed
$tree = &drupal_static(__FUNCTION__, array());

Dries Buytaert
committed

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

Angie Byron
committed
$mlid = isset($link['mlid']) ? $link['mlid'] : 0;

Dries Buytaert
committed
// Generate a cache ID (cid) specific for this $menu_name, $link, $language, and depth.
$cid = 'links:' . $menu_name . ':all-cid:' . $mlid . ':' . $GLOBALS['language']->language . ':' . (int) $max_depth;

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 contains the parameters for
// menu_build_tree().
$tree_parameters = $cache->data;