Skip to content
Snippets Groups Projects
menu.inc 64.9 KiB
Newer Older
Dries Buytaert's avatar
Dries Buytaert committed
<?php
/**
 * @file
 * API for the Drupal menu system.
 */

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
 * @defgroup menu Menu system
 * @{
Dries Buytaert's avatar
 
Dries Buytaert committed
 * Define the navigation menus, and route page requests to code based on URLs.
Dries Buytaert's avatar
 
Dries Buytaert committed
 *
 * 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.
Dries Buytaert's avatar
 
Dries Buytaert committed
 *
 * 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.
Dries Buytaert's avatar
 
Dries Buytaert committed
 *
 * 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().
Dries Buytaert's avatar
 
Dries Buytaert committed
 */

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
Dries Buytaert's avatar
 
Dries Buytaert committed
 * @name Menu flags
Dries Buytaert's avatar
 
Dries Buytaert committed
 * @{
Dries Buytaert's avatar
 
Dries Buytaert committed
 * Flags for use in the "type" attribute of menu items.
 */
Dries Buytaert's avatar
 
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
define('MENU_IS_ROOT', 0x0001);
define('MENU_VISIBLE_IN_TREE', 0x0002);
define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004);
define('MENU_LINKS_TO_PARENT', 0x0008);
Dries Buytaert's avatar
 
Dries Buytaert committed
define('MENU_MODIFIABLE_BY_ADMIN', 0x0010);
define('MENU_MODIFIED_BY_ADMIN', 0x0020);
define('MENU_CREATED_BY_ADMIN', 0x0040);
define('MENU_IS_LOCAL_TASK', 0x0080);
Dries Buytaert's avatar
 
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
Dries Buytaert's avatar
 
Dries Buytaert committed
 * @} End of "Menu flags".
Dries Buytaert's avatar
 
Dries Buytaert committed
 */

/**
Dries Buytaert's avatar
 
Dries Buytaert committed
 * @name Menu item types
Dries Buytaert's avatar
 
Dries Buytaert committed
 * @{
 * Menu item definitions provide one of these constants, which are shortcuts for
 * combinations of the above flags.
 */
Dries Buytaert's avatar
 
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
 * Normal menu items show up in the menu tree and can be moved/hidden by
Dries Buytaert's avatar
 
Dries Buytaert committed
 * the administrator. Use this for most menu items. It is the default value if
 * no menu item type is specified.
Dries Buytaert's avatar
 
Dries Buytaert committed
 */
define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB | MENU_MODIFIABLE_BY_ADMIN);
Dries Buytaert's avatar
 
Dries Buytaert committed
/**
 * Callbacks simply register a path so that the correct function is fired
Dries Buytaert's avatar
 
Dries Buytaert committed
 * when the URL is accessed. They are not shown in the menu.
Dries Buytaert's avatar
 
Dries Buytaert committed
 */
define('MENU_CALLBACK', MENU_VISIBLE_IN_BREADCRUMB);
Dries Buytaert's avatar
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
Dries Buytaert's avatar
 
Dries Buytaert committed
 * 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.
Dries Buytaert's avatar
 
Dries Buytaert committed
 */
define('MENU_SUGGESTED_ITEM', MENU_MODIFIABLE_BY_ADMIN | MENU_VISIBLE_IN_BREADCRUMB);
Dries Buytaert's avatar
 
Dries Buytaert committed

/**
Dries Buytaert's avatar
 
Dries Buytaert committed
 * 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".
Dries Buytaert's avatar
 
Dries Buytaert committed
 */
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK);

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
 * 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);

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
Dries Buytaert's avatar
 
Dries Buytaert committed
 * @} End of "Menu item types".
Dries Buytaert's avatar
 
Dries Buytaert committed
 */

/**
 * @name Menu status codes
 * @{
Dries Buytaert's avatar
 
Dries Buytaert committed
 * Status codes for menu callbacks.
 */
Dries Buytaert's avatar
 
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
define('MENU_FOUND', 1);
define('MENU_NOT_FOUND', 2);
define('MENU_ACCESS_DENIED', 3);
Dries Buytaert's avatar
 
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
Dries Buytaert's avatar
 
Dries Buytaert committed
 * @} End of "Menu status codes".
Dries Buytaert's avatar
 
Dries Buytaert committed
 */
 * @Name Menu tree parameters
 /**
 * The maximum number of path elements for a menu callback
 * The maximum depth of a menu links tree - matches the number of p columns.
 * @} End of "Menu tree parameters".
 * 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
 *
 * 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().
 *
 * @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.
  $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;
    }
    $current = '';
    for ($j = $length; $j >= 0; $j--) {
      if ($i & (1 << $j)) {
        $current .= $parts[$length - $j];
      }
      else {
        $current .= '%';
      }
      if ($j) {
        $current .= '/';
      }
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
    $placeholders[] = "'%s'";
    $ancestors[] = $current;
Dries Buytaert's avatar
 
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's avatar
 
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').
 * @param @data
 *   A serialized array.
 * @param @map
 *   An array of potential replacements.
 *   The $data array unserialized and mapped.
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;
 * Get the menu callback for the a path.
 *   A path, or NULL for the current path
function menu_get_item($path = NULL) {
    $original_map = arg(NULL, $path);
    $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
    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))) {
      $map = _menu_translate($router_item, $original_map);
      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's avatar
 
Dries Buytaert committed
      }
    }
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed
}

/**
 * Execute the page callback associated with the current path
Dries Buytaert's avatar
 
Dries Buytaert committed
 */
function menu_execute_active_handler($path = NULL) {
  if (_menu_site_is_offline()) {
    return MENU_SITE_OFFLINE;
  }
  if ($router_item = menu_get_item($path)) {
    if ($router_item['access']) {
      if ($router_item['file']) {
        require_once($router_item['file']);
      return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed

 * Loads objects into the map as defined in the $item['load_functions'].
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
 *   Returns TRUE for success, FALSE if an object cannot be loaded
 */
function _menu_load_objects($item, &$map) {
  if ($item['load_functions']) {
    $load_functions = unserialize($item['load_functions']);
    $path_map = $map;
    foreach ($load_functions as $index => $function) {
      if ($function) {

        $return = $function(isset($path_map[$index]) ? $path_map[$index] : '');
        // If callback returned an error or there is no callback, trigger 404.
        if ($return === FALSE) {
          return FALSE;
  return TRUE;
}

/**
 * Check access to a menu item using the access callback
 *
 * @param $item
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
 *   $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 = trim($item['access_callback']);
  // Check for a TRUE or FALSE value.
Dries Buytaert's avatar
 
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') {
      $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
      $item['access'] = call_user_func_array($callback, $arguments);
Dries Buytaert's avatar
 
Dries Buytaert committed
  }
/**
 * Localize the item title using t() or another callback.
 */
function _menu_item_localize(&$item, $map) {
  // Translate the title to allow storage of English title strings in the
  // database, yet display of them in the language required by the current
  // user.
  // 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']);
      $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
    if (empty($item['title_arguments'])) {
      $item['title'] = $callback($item['title']);
      $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
    }
  }

  // Translate description, see the motivation above.
  if (!empty($item['description'])) {
    $item['description'] = t($item['description']);
}

/**
 * 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
 *
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @param $to_arg
 *   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
 *   $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.
 */
function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
  if (!_menu_load_objects($router_item, $map)) {
    // An error occurred loading an object.
    _menu_link_map_translate($path_map, $router_item['to_arg_functions']);
  }

  // Generate the link path for the page request or local tasks.
  $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];
    }
  }
  $router_item['href'] = implode('/', $link_map);
  _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(1 => 'node_load'))
 */
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] : '');
      if (!empty($map[$index]) || isset($arg)) {
        $map[$index] = $arg;
      }
      else {
        unset($map[$index]);
      }
    }
  }
}

/**
 * This function is similar to _menu_translate() but does link-specific
 * preparation such as always calling to_arg functions
 *
 * @param $item
 * @return
 *   Returns the map of path arguments with objects loaded as defined in the
 *   $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.
 */
function _menu_link_translate(&$item) {
  if ($item['external']) {
    $item['access'] = 1;
    $item['href'] = $item['link_path'];
    $item['title'] = $item['link_title'];
    $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.
    if (strpos($item['href'], '%') !== FALSE) {
      $item['access'] = FALSE;
    // menu_tree_check_access() may set this ahead of time for links to nodes.
    if (!isset($item['access'])) {
      if (!_menu_load_objects($item, $map)) {
      _menu_check_access($item, $map);
    }
    // If the link title matches that of a router item, localize it.
    if (!empty($item['title']) && (($item['title'] == $item['link_title']) || ($item['title_callback'] != 't'))) {
      _menu_item_localize($item, $map);
  $item['options'] = unserialize($item['options']);
Dries Buytaert's avatar
 
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])) {
    $tree = menu_tree_page_data($menu_name);
    $menu_output[$menu_name] = menu_tree_output($tree);
  }
  return $menu_output[$menu_name];
}

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

  foreach ($tree as $data) {
      $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']);
        $output .= theme('menu_item', $link, $data['link']['has_children'], '', $data['link']['in_active_trail']);
  }
  return $output ? theme('menu_tree', $output) : '';
}

 * 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.
 *
 * @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) {
  // Use $mlid as a flag for whether the data being loaded is for the whole tree.
  $mlid = isset($item['mlid']) ? $item['mlid'] : 0;
  $cid = 'links:'. $menu_name .':all:'. $mlid;
    // If the static variable doesn't have the data, check {cache_menu}.
    $cache = cache_get($cid, 'cache_menu');
    if ($cache && isset($cache->data)) {
        // 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"];
        }
        $args = array_unique($args);
        $placeholders = implode(', ', array_fill(0, count($args), '%d'));
        $where = ' AND ml.plid IN ('. $placeholders .')';
        $parents = $args;
        $parents[] = $item['mlid'];
      }
      else {
        $where = '';
        $args = array();
        $parents = array();
      }
      array_unshift($args, $menu_name);
      // 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
      $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, 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);
      $data['node_links'] = array();
      menu_tree_collect_node_links($data['tree'], $data['node_links']);
    // Check access for the current user to each item in the tree.
    menu_tree_check_access($data['tree'], $data['node_links']);
 * 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.
function menu_tree_page_data($menu_name = 'navigation') {
  // Load the menu item corresponding to the current page.
  if ($item = menu_get_item()) {
    $cid = 'links:'. $menu_name .':page:'. $item['href'] .':'. (int)$item['access'];
      // If the static variable doesn't have the data, check {cache_menu}.
      $cache = cache_get($cid, 'cache_menu');
      if ($cache && isset($cache->data)) {
          // Check whether a menu link exists that corresponds to the current path.
          $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['href']));
            // 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']));
          // We always want all the top-level links with plid == 0.
          // Use array_values() so that the indices are numeric for array_merge().
          $args = $parents = array_unique(array_values($parents));
          $placeholders = implode(', ', array_fill(0, count($args), '%d'));
          $expanded = variable_get('menu_expanded', array());
          // Check whether the current menu has any links set to be expanded.
            // Collect all the links set to be expanded, and then add all of
            // their children to the list as well.
              $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));
              while ($item = db_fetch_array($result)) {
                $args[] = $item['mlid'];
              }
              $placeholders = implode(', ', array_fill(0, count($args), '%d'));
          // Show only the top-level menu items when access is denied.
          $args = array($menu_name, '0');
        // 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
        $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, 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);
        $data['node_links'] = array();
        menu_tree_collect_node_links($data['tree'], $data['node_links']);
      // Check access for the current user to each item in the tree.
      menu_tree_check_access($data['tree'], $data['node_links']);
      $tree[$cid] = $data['tree'];
Dries Buytaert's avatar
 
Dries Buytaert committed
}

/**
 * Recursive helper function - collect node links.
 */
function menu_tree_collect_node_links(&$tree, &$node_links) {

  foreach ($tree as $key => $v) {
    if ($tree[$key]['link']['router_path'] == 'node/%') {
      $nid = substr($tree[$key]['link']['link_path'], 5);
      if (is_numeric($nid)) {
        $node_links[$nid] = &$tree[$key]['link'];
        $tree[$key]['link']['access'] = FALSE;
      }
    }
    if ($tree[$key]['below']) {
      menu_tree_collect_node_links($tree[$key]['below'], $node_links);
/**
 * Check access and perform other dynamic operations for each link in the tree.
 */
function menu_tree_check_access(&$tree, $node_links = array()) {

  if ($node_links) {
    // Use db_rewrite_sql to evaluate view access without loading each full node.
    $nids = array_keys($node_links);
    $placeholders = '%d'. str_repeat(', %d', count($nids) - 1);
    $result = db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.nid IN (". $placeholders .")"), $nids);
    while ($node = db_fetch_array($result)) {
      $node_links[$node['nid']]['access'] = TRUE;
    }
  }
}

/**
 * Recursive helper function for menu_tree_check_access()
 */
function _menu_tree_check_access(&$tree) {
  foreach ($tree as $key => $v) {
    $item = &$tree[$key]['link'];
        _menu_tree_check_access($tree[$key]['below']);
      }
      // The weights are made a uniform 5 digits by adding 50000 as an offset.
      // After _menu_link_translate(), $item['title'] has the localized link title.
      // Adding the mlid to the end of the index insures that it is unique.
      $new_tree[(50000 + $item['weight']) .' '. $item['title'] .' '. $item['mlid']] = $tree[$key];
  // Sort siblings in the tree based on the weights and localized titles.
  ksort($new_tree);
  $tree = $new_tree;
 * Build the data representing a menu tree.
 *
 * @param $result
 *   The database result.
 * @param $parents
 *   An array of the plid values that represent the path from the current page
 *   to the root of the menu tree.
 * @param $depth
 *   The depth of the current menu tree.
 * @return
 *   See menu_tree_page_data for a description of the data structure.
 */
function menu_tree_data($result = NULL, $parents = array(), $depth = 1) {

  list(, $tree) = _menu_tree_data($result, $parents, $depth);
  return $tree;
}

/**
 * Recursive helper function to build the data representing a menu tree.
 *
 * The function is a bit complex because the rendering of an item depends on
 * the next menu item. So we are always rendering the element previously
 * processed not the current one.
function _menu_tree_data($result, $parents, $depth, $previous_element = '') {
  while ($item = db_fetch_array($result)) {
    // We need to determine if we're on the path to root so we can later build
    // the correct active trail and breadcrumb.
    $item['in_active_trail'] = in_array($item['mlid'], $parents);

    $index = $previous_element ? ($previous_element['mlid']) : '';
    // The current item is the first in a new submenu.
      // _menu_tree returns an item and the menu tree structure.
      list($item, $below) = _menu_tree_data($result, $parents, $item['depth'], $item);
      $tree[$index] = array(
        'link' => $previous_element,
        'below' => $below,
      );
      // We need to fall back one level.
      if (!isset($item) || $item['depth'] < $depth) {
      // This will be the link to be output in the next iteration.
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
    // We are at the same depth, so we use the previous element.
      if ($previous_element) {
        // Only the first time.
        $tree[$index] = array(
          'link' => $previous_element,
      // This will be the link to be output in the next iteration.
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
    // The submenu ended with the previous item, so pass back the current item.
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
Dries Buytaert's avatar
Dries Buytaert committed
  }
      'link' => $previous_element,
Dries Buytaert's avatar
Dries Buytaert committed
}

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
 * Generate the HTML output for a single menu link.
Dries Buytaert's avatar
 
Dries Buytaert committed
 */
function theme_menu_item_link($link) {
  return l($link['title'], $link['href'], $link['options']);
Dries Buytaert's avatar
 
Dries Buytaert committed
}

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
 * Generate the HTML output for a menu tree
Dries Buytaert's avatar
 
Dries Buytaert committed
 */
function theme_menu_tree($tree) {
  return '<ul class="menu">'. $tree .'</ul>';
}

/**
 * Generate the HTML output for a menu item and submenu.
 */
function theme_menu_item($link, $has_children, $menu = '', $in_active_trail = FALSE) {
  $class = ($menu ? 'expanded' : ($has_children ? 'collapsed' : 'leaf'));
  if ($in_active_trail) {
    $class .= ' active-trail';
  }
  return '<li class="'. $class .'">'. $link . $menu .'</li>'."\n";
/**
 * Generate the HTML output for a single local task link.
 */
function theme_menu_local_task($link, $active = FALSE) {
  return '<li '. ($active ? 'class="active" ' : '') .'>'. $link .'</li>';
Dries Buytaert's avatar
Dries Buytaert committed

/**
 * Generates elements for the $arg array in the help hook.
 */
function drupal_help_arg($arg = array()) {
  // Note - the number of empty elements should be > MENU_MAX_PARTS.
  return $arg + array('', '', '', '', '', '', '', '', '', '', '', '');
}

Dries Buytaert's avatar
 
Dries Buytaert committed
/**
Dries Buytaert's avatar
 
Dries Buytaert committed
 * Returns the help associated with the active menu item.
 */
Dries Buytaert's avatar
 
Dries Buytaert committed
  $output = '';
Dries Buytaert's avatar
Dries Buytaert committed

  $arg = drupal_help_arg(arg(NULL));
  $empty_arg = drupal_help_arg();
Dries Buytaert's avatar
 
Dries Buytaert committed

Dries Buytaert's avatar
 
Dries Buytaert committed
  foreach (module_list() as $name) {
    if (module_hook($name, 'help')) {
      // Lookup help for this path.
      if ($help = module_invoke($name, 'help', $router_path, $arg)) {
        $output .= $help ."\n";
Dries Buytaert's avatar
 
Dries Buytaert committed
      }
      // Add "more help" link on admin pages if the module provides a
      // standalone help page.
      if ($arg[0] == "admin" && module_exists('help') && module_invoke($name, 'help', 'admin/help#'. $arg[2], $empty_arg) && $help) {
        $output .= theme("more_help_link", url('admin/help/'. $arg[2]));
Dries Buytaert's avatar
 
Dries Buytaert committed
      }
Dries Buytaert's avatar
 
Dries Buytaert committed
    }
Dries Buytaert's avatar
Dries Buytaert committed
  }
Dries Buytaert's avatar
 
Dries Buytaert committed
  return $output;
 * Build a list of named menus.
function menu_get_names($reset = FALSE) {
  static $names;

  if ($reset || empty($names)) {
    $names = array();
    $result = db_query("SELECT DISTINCT(menu_name) FROM {menu_links} ORDER BY menu_name");
    while ($name = db_fetch_array($result)) {
      $names[] = $name['menu_name'];
/**
 * Return an array containing the names of system-defined (default) menus.
 */
function menu_list_system_menus() {
  return array('navigation', 'primary-links', 'secondary-links');
}

/**
 * Return an array of links to be rendered as the Primary links.
 */
  return menu_navigation_links('primary-links');
Dries Buytaert's avatar
 
Dries Buytaert committed
}
/**
 * Return an array of links to be rendered as the Secondary links.
 */