home *** CD-ROM | disk | FTP | other *** search
/ Cricao de Sites - 650 Layouts Prontos / WebMasters.iso / CMS / drupal-6.0.exe / drupal-6.0 / includes / menu.inc < prev    next >
Encoding:
Text File  |  2008-02-13  |  81.3 KB  |  2,414 lines

  1. <?php
  2. // $Id: menu.inc,v 1.255.2.7 2008/02/12 22:33:51 goba Exp $
  3.  
  4. /**
  5.  * @file
  6.  * API for the Drupal menu system.
  7.  */
  8.  
  9. /**
  10.  * @defgroup menu Menu system
  11.  * @{
  12.  * Define the navigation menus, and route page requests to code based on URLs.
  13.  *
  14.  * The Drupal menu system drives both the navigation system from a user
  15.  * perspective and the callback system that Drupal uses to respond to URLs
  16.  * passed from the browser. For this reason, a good understanding of the
  17.  * menu system is fundamental to the creation of complex modules.
  18.  *
  19.  * Drupal's menu system follows a simple hierarchy defined by paths.
  20.  * Implementations of hook_menu() define menu items and assign them to
  21.  * paths (which should be unique). The menu system aggregates these items
  22.  * and determines the menu hierarchy from the paths. For example, if the
  23.  * paths defined were a, a/b, e, a/b/c/d, f/g, and a/b/h, the menu system
  24.  * would form the structure:
  25.  * - a
  26.  *   - a/b
  27.  *     - a/b/c/d
  28.  *     - a/b/h
  29.  * - e
  30.  * - f/g
  31.  * Note that the number of elements in the path does not necessarily
  32.  * determine the depth of the menu item in the tree.
  33.  *
  34.  * When responding to a page request, the menu system looks to see if the
  35.  * path requested by the browser is registered as a menu item with a
  36.  * callback. If not, the system searches up the menu tree for the most
  37.  * complete match with a callback it can find. If the path a/b/i is
  38.  * requested in the tree above, the callback for a/b would be used.
  39.  *
  40.  * The found callback function is called with any arguments specified
  41.  * in the "page arguments" attribute of its menu item. The
  42.  * attribute must be an array. After these arguments, any remaining
  43.  * components of the path are appended as further arguments. In this
  44.  * way, the callback for a/b above could respond to a request for
  45.  * a/b/i differently than a request for a/b/j.
  46.  *
  47.  * For an illustration of this process, see page_example.module.
  48.  *
  49.  * Access to the callback functions is also protected by the menu system.
  50.  * The "access callback" with an optional "access arguments" of each menu
  51.  * item is called before the page callback proceeds. If this returns TRUE,
  52.  * then access is granted; if FALSE, then access is denied. Menu items may
  53.  * omit this attribute to use the value provided by an ancestor item.
  54.  *
  55.  * In the default Drupal interface, you will notice many links rendered as
  56.  * tabs. These are known in the menu system as "local tasks", and they are
  57.  * rendered as tabs by default, though other presentations are possible.
  58.  * Local tasks function just as other menu items in most respects. It is
  59.  * convention that the names of these tasks should be short verbs if
  60.  * possible. In addition, a "default" local task should be provided for
  61.  * each set. When visiting a local task's parent menu item, the default
  62.  * local task will be rendered as if it is selected; this provides for a
  63.  * normal tab user experience. This default task is special in that it
  64.  * links not to its provided path, but to its parent item's path instead.
  65.  * The default task's path is only used to place it appropriately in the
  66.  * menu hierarchy.
  67.  *
  68.  * Everything described so far is stored in the menu_router table. The
  69.  * menu_links table holds the visible menu links. By default these are
  70.  * derived from the same hook_menu definitions, however you are free to
  71.  * add more with menu_link_save().
  72.  */
  73.  
  74. /**
  75.  * @name Menu flags
  76.  * @{
  77.  * Flags for use in the "type" attribute of menu items.
  78.  */
  79.  
  80. define('MENU_IS_ROOT', 0x0001);
  81. define('MENU_VISIBLE_IN_TREE', 0x0002);
  82. define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004);
  83. define('MENU_LINKS_TO_PARENT', 0x0008);
  84. define('MENU_MODIFIED_BY_ADMIN', 0x0020);
  85. define('MENU_CREATED_BY_ADMIN', 0x0040);
  86. define('MENU_IS_LOCAL_TASK', 0x0080);
  87.  
  88. /**
  89.  * @} End of "Menu flags".
  90.  */
  91.  
  92. /**
  93.  * @name Menu item types
  94.  * @{
  95.  * Menu item definitions provide one of these constants, which are shortcuts for
  96.  * combinations of the above flags.
  97.  */
  98.  
  99. /**
  100.  * Normal menu items show up in the menu tree and can be moved/hidden by
  101.  * the administrator. Use this for most menu items. It is the default value if
  102.  * no menu item type is specified.
  103.  */
  104. define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);
  105.  
  106. /**
  107.  * Callbacks simply register a path so that the correct function is fired
  108.  * when the URL is accessed. They are not shown in the menu.
  109.  */
  110. define('MENU_CALLBACK', MENU_VISIBLE_IN_BREADCRUMB);
  111.  
  112. /**
  113.  * Modules may "suggest" menu items that the administrator may enable. They act
  114.  * just as callbacks do until enabled, at which time they act like normal items.
  115.  * Note for the value: 0x0010 was a flag which is no longer used, but this way
  116.  * the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate.
  117.  */
  118. define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010);
  119.  
  120. /**
  121.  * Local tasks are rendered as tabs by default. Use this for menu items that
  122.  * describe actions to be performed on their parent item. An example is the path
  123.  * "node/52/edit", which performs the "edit" task on "node/52".
  124.  */
  125. define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK);
  126.  
  127. /**
  128.  * Every set of local tasks should provide one "default" task, that links to the
  129.  * same path as its parent when clicked.
  130.  */
  131. define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT);
  132.  
  133. /**
  134.  * @} End of "Menu item types".
  135.  */
  136.  
  137. /**
  138.  * @name Menu status codes
  139.  * @{
  140.  * Status codes for menu callbacks.
  141.  */
  142.  
  143. define('MENU_FOUND', 1);
  144. define('MENU_NOT_FOUND', 2);
  145. define('MENU_ACCESS_DENIED', 3);
  146. define('MENU_SITE_OFFLINE', 4);
  147.  
  148. /**
  149.  * @} End of "Menu status codes".
  150.  */
  151.  
  152. /**
  153.  * @Name Menu tree parameters
  154.  * @{
  155.  * Menu tree
  156.  */
  157.  
  158.  /**
  159.  * The maximum number of path elements for a menu callback
  160.  */
  161. define('MENU_MAX_PARTS', 7);
  162.  
  163.  
  164. /**
  165.  * The maximum depth of a menu links tree - matches the number of p columns.
  166.  */
  167. define('MENU_MAX_DEPTH', 9);
  168.  
  169.  
  170. /**
  171.  * @} End of "Menu tree parameters".
  172.  */
  173.  
  174. /**
  175.  * Returns the ancestors (and relevant placeholders) for any given path.
  176.  *
  177.  * For example, the ancestors of node/12345/edit are:
  178.  *
  179.  * node/12345/edit
  180.  * node/12345/%
  181.  * node/%/edit
  182.  * node/%/%
  183.  * node/12345
  184.  * node/%
  185.  * node
  186.  *
  187.  * To generate these, we will use binary numbers. Each bit represents a
  188.  * part of the path. If the bit is 1, then it represents the original
  189.  * value while 0 means wildcard. If the path is node/12/edit/foo
  190.  * then the 1011 bitstring represents node/%/edit/foo where % means that
  191.  * any argument matches that part.  We limit ourselves to using binary
  192.  * numbers that correspond the patterns of wildcards of router items that
  193.  * actually exists.  This list of 'masks' is built in menu_rebuild().
  194.  *
  195.  * @param $parts
  196.  *   An array of path parts, for the above example
  197.  *   array('node', '12345', 'edit').
  198.  * @return
  199.  *   An array which contains the ancestors and placeholders. Placeholders
  200.  *   simply contain as many '%s' as the ancestors.
  201.  */
  202. function menu_get_ancestors($parts) {
  203.   $number_parts = count($parts);
  204.   $placeholders = array();
  205.   $ancestors = array();
  206.   $length =  $number_parts - 1;
  207.   $end = (1 << $number_parts) - 1;
  208.   $masks = variable_get('menu_masks', array());
  209.   // Only examine patterns that actually exist as router items (the masks).
  210.   foreach ($masks as $i) {
  211.     if ($i > $end) {
  212.       // Only look at masks that are not longer than the path of interest.
  213.       continue;
  214.     }
  215.     elseif ($i < (1 << $length)) {
  216.       // We have exhausted the masks of a given length, so decrease the length.
  217.       --$length;
  218.     }
  219.     $current = '';
  220.     for ($j = $length; $j >= 0; $j--) {
  221.       if ($i & (1 << $j)) {
  222.         $current .= $parts[$length - $j];
  223.       }
  224.       else {
  225.         $current .= '%';
  226.       }
  227.       if ($j) {
  228.         $current .= '/';
  229.       }
  230.     }
  231.     $placeholders[] = "'%s'";
  232.     $ancestors[] = $current;
  233.   }
  234.   return array($ancestors, $placeholders);
  235. }
  236.  
  237. /**
  238.  * The menu system uses serialized arrays stored in the database for
  239.  * arguments. However, often these need to change according to the
  240.  * current path. This function unserializes such an array and does the
  241.  * necessary change.
  242.  *
  243.  * Integer values are mapped according to the $map parameter. For
  244.  * example, if unserialize($data) is array('view', 1) and $map is
  245.  * array('node', '12345') then 'view' will not be changed because
  246.  * it is not an integer, but 1 will as it is an integer. As $map[1]
  247.  * is '12345', 1 will be replaced with '12345'. So the result will
  248.  * be array('node_load', '12345').
  249.  *
  250.  * @param @data
  251.  *   A serialized array.
  252.  * @param @map
  253.  *   An array of potential replacements.
  254.  * @return
  255.  *   The $data array unserialized and mapped.
  256.  */
  257. function menu_unserialize($data, $map) {
  258.   if ($data = unserialize($data)) {
  259.     foreach ($data as $k => $v) {
  260.       if (is_int($v)) {
  261.         $data[$k] = isset($map[$v]) ? $map[$v] : '';
  262.       }
  263.     }
  264.     return $data;
  265.   }
  266.   else {
  267.     return array();
  268.   }
  269. }
  270.  
  271.  
  272.  
  273. /**
  274.  * Replaces the statically cached item for a given path.
  275.  *
  276.  * @param $path
  277.  *   The path.
  278.  * @param $router_item
  279.  *   The router item. Usually you take a router entry from menu_get_item and
  280.  *   set it back either modified or to a different path. This lets you modify the
  281.  *   navigation block, the page title, the breadcrumb and the page help in one
  282.  *   call.
  283.  */
  284. function menu_set_item($path, $router_item) {
  285.   menu_get_item($path, $router_item);
  286. }
  287.  
  288. /**
  289.  * Get a router item.
  290.  *
  291.  * @param $path
  292.  *   The path, for example node/5. The function will find the corresponding
  293.  *   node/% item and return that.
  294.  * @param $router_item
  295.  *   Internal use only.
  296.  * @return
  297.  *   The router item, an associate array corresponding to one row in the
  298.  *   menu_router table. The value of key map holds the loaded objects. The
  299.  *   value of key access is TRUE if the current user can access this page.
  300.  *   The values for key title, page_arguments, access_arguments will be
  301.  *   filled in based on the database values and the objects loaded.
  302.  */
  303. function menu_get_item($path = NULL, $router_item = NULL) {
  304.   static $router_items;
  305.   if (!isset($path)) {
  306.     $path = $_GET['q'];
  307.   }
  308.   if (isset($router_item)) {
  309.     $router_items[$path] = $router_item;
  310.   }
  311.   if (!isset($router_items[$path])) {
  312.     $original_map = arg(NULL, $path);
  313.     $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
  314.     list($ancestors, $placeholders) = menu_get_ancestors($parts);
  315.  
  316.     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))) {
  317.       $map = _menu_translate($router_item, $original_map);
  318.       if ($map === FALSE) {
  319.         $router_items[$path] = FALSE;
  320.         return FALSE;
  321.       }
  322.       if ($router_item['access']) {
  323.         $router_item['map'] = $map;
  324.         $router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $map), array_slice($map, $router_item['number_parts']));
  325.       }
  326.     }
  327.     $router_items[$path] = $router_item;
  328.   }
  329.   return $router_items[$path];
  330. }
  331.  
  332. /**
  333.  * Execute the page callback associated with the current path
  334.  */
  335. function menu_execute_active_handler($path = NULL) {
  336.   if (_menu_site_is_offline()) {
  337.     return MENU_SITE_OFFLINE;
  338.   }
  339.   if (variable_get('menu_rebuild_needed', FALSE)) {
  340.     menu_rebuild();
  341.   }
  342.   if ($router_item = menu_get_item($path)) {
  343.     if ($router_item['access']) {
  344.       if ($router_item['file']) {
  345.         require_once($router_item['file']);
  346.       }
  347.       return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
  348.     }
  349.     else {
  350.       return MENU_ACCESS_DENIED;
  351.     }
  352.   }
  353.   return MENU_NOT_FOUND;
  354. }
  355.  
  356. /**
  357.  * Loads objects into the map as defined in the $item['load_functions'].
  358.  *
  359.  * @param $item
  360.  *   A menu router or menu link item
  361.  * @param $map
  362.  *   An array of path arguments (ex: array('node', '5'))
  363.  * @return
  364.  *   Returns TRUE for success, FALSE if an object cannot be loaded.
  365.  *   Names of object loading functions are placed in $item['load_functions'].
  366.  *   Loaded objects are placed in $map[]; keys are the same as keys in the 
  367.  *   $item['load_functions'] array.
  368.  *   $item['access'] is set to FALSE if an object cannot be loaded.
  369.  */
  370. function _menu_load_objects(&$item, &$map) {
  371.   if ($load_functions = $item['load_functions']) {
  372.     // If someone calls this function twice, then unserialize will fail.
  373.     if ($load_functions_unserialized = unserialize($load_functions)) {
  374.       $load_functions = $load_functions_unserialized;
  375.     }
  376.     $path_map = $map;
  377.     foreach ($load_functions as $index => $function) {
  378.       if ($function) {
  379.         $value = isset($path_map[$index]) ? $path_map[$index] : '';
  380.         if (is_array($function)) {
  381.           // Set up arguments for the load function. These were pulled from
  382.           // 'load arguments' in the hook_menu() entry, but they need
  383.           // some processing. In this case the $function is the key to the
  384.           // load_function array, and the value is the list of arguments.
  385.           list($function, $args) = each($function);
  386.           $load_functions[$index] = $function;
  387.  
  388.           // Some arguments are placeholders for dynamic items to process.
  389.           foreach ($args as $i => $arg) {
  390.             if ($arg === '%index') {
  391.               // Pass on argument index to the load function, so multiple
  392.               // occurances of the same placeholder can be identified.
  393.               $args[$i] = $index;
  394.             }
  395.             if ($arg === '%map') {
  396.               // Pass on menu map by reference. The accepting function must
  397.               // also declare this as a reference if it wants to modify
  398.               // the map.
  399.               $args[$i] = &$map;
  400.             }
  401.             if (is_int($arg)) {
  402.               $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : '';
  403.             }
  404.           }
  405.           array_unshift($args, $value);
  406.           $return = call_user_func_array($function, $args);
  407.         }
  408.         else {
  409.           $return = $function($value);
  410.         }
  411.         // If callback returned an error or there is no callback, trigger 404.
  412.         if ($return === FALSE) {
  413.           $item['access'] = FALSE;
  414.           $map = FALSE;
  415.           return FALSE;
  416.         }
  417.         $map[$index] = $return;
  418.       }
  419.     }
  420.     $item['load_functions'] = $load_functions;
  421.   }
  422.   return TRUE;
  423. }
  424.  
  425. /**
  426.  * Check access to a menu item using the access callback
  427.  *
  428.  * @param $item
  429.  *   A menu router or menu link item
  430.  * @param $map
  431.  *   An array of path arguments (ex: array('node', '5'))
  432.  * @return
  433.  *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
  434.  */
  435. function _menu_check_access(&$item, $map) {
  436.   // Determine access callback, which will decide whether or not the current
  437.   // user has access to this path.
  438.   $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']);
  439.   // Check for a TRUE or FALSE value.
  440.   if (is_numeric($callback)) {
  441.     $item['access'] = (bool)$callback;
  442.   }
  443.   else {
  444.     $arguments = menu_unserialize($item['access_arguments'], $map);
  445.     // As call_user_func_array is quite slow and user_access is a very common
  446.     // callback, it is worth making a special case for it.
  447.     if ($callback == 'user_access') {
  448.       $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
  449.     }
  450.     else {
  451.       $item['access'] = call_user_func_array($callback, $arguments);
  452.     }
  453.   }
  454. }
  455.  
  456. /**
  457.  * Localize the router item title using t() or another callback.
  458.  *
  459.  * Translate the title and description to allow storage of English title
  460.  * strings in the database, yet display of them in the language required
  461.  * by the current user.
  462.  *
  463.  * @param $item
  464.  *   A menu router item or a menu link item.
  465.  * @param $map
  466.  *   The path as an array with objects already replaced. E.g., for path
  467.  *   node/123 $map would be array('node', $node) where $node is the node
  468.  *   object for node 123.
  469.  * @param $link_translate
  470.  *   TRUE if we are translating a menu link item; FALSE if we are
  471.  *   translating a menu router item.
  472.  * @return
  473.  *   No return value.
  474.  *   $item['title'] is localized according to $item['title_callback'].
  475.  *   If an item's callback is check_plain(), $item['options']['html'] becomes 
  476.  *   TRUE.
  477.  *   $item['description'] is translated using t().
  478.  *   When doing link translation and the $item['options']['attributes']['title'] 
  479.  *   (link title attribute) matches the description, it is translated as well.
  480.  */
  481. function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
  482.   $callback = $item['title_callback'];
  483.   $item['localized_options'] = $item['options'];
  484.   // If we are not doing link translation or if the title matches the
  485.   // link title of its router item, localize it.
  486.   if (!$link_translate || (!empty($item['title']) && ($item['title'] == $item['link_title']))) {
  487.     // t() is a special case. Since it is used very close to all the time,
  488.     // we handle it directly instead of using indirect, slower methods.
  489.     if ($callback == 't') {
  490.       if (empty($item['title_arguments'])) {
  491.         $item['title'] = t($item['title']);
  492.       }
  493.       else {
  494.         $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
  495.       }
  496.     }
  497.     elseif ($callback) {
  498.       if (empty($item['title_arguments'])) {
  499.         $item['title'] = $callback($item['title']);
  500.       }
  501.       else {
  502.         $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
  503.       }
  504.       // Avoid calling check_plain again on l() function.
  505.       if ($callback == 'check_plain') {
  506.         $item['localized_options']['html'] = TRUE;
  507.       }
  508.     }
  509.   }
  510.   elseif ($link_translate) {
  511.     $item['title'] = $item['link_title'];
  512.   }
  513.  
  514.   // Translate description, see the motivation above.
  515.   if (!empty($item['description'])) {
  516.     $original_description = $item['description'];
  517.     $item['description'] = t($item['description']);
  518.     if ($link_translate && $item['options']['attributes']['title'] == $original_description) {
  519.       $item['localized_options']['attributes']['title'] = $item['description'];
  520.     }
  521.   }
  522. }
  523.  
  524. /**
  525.  * Handles dynamic path translation and menu access control.
  526.  *
  527.  * When a user arrives on a page such as node/5, this function determines
  528.  * what "5" corresponds to, by inspecting the page's menu path definition,
  529.  * node/%node. This will call node_load(5) to load the corresponding node
  530.  * object.
  531.  *
  532.  * It also works in reverse, to allow the display of tabs and menu items which
  533.  * contain these dynamic arguments, translating node/%node to node/5.
  534.  *
  535.  * Translation of menu item titles and descriptions are done here to
  536.  * allow for storage of English strings in the database, and translation
  537.  * to the language required to generate the current page
  538.  *
  539.  * @param $router_item
  540.  *   A menu router item
  541.  * @param $map
  542.  *   An array of path arguments (ex: array('node', '5'))
  543.  * @param $to_arg
  544.  *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
  545.  *   path from the menu table, for example tabs.
  546.  * @return
  547.  *   Returns the map with objects loaded as defined in the
  548.  *   $item['load_functions. $item['access'] becomes TRUE if the item is
  549.  *   accessible, FALSE otherwise. $item['href'] is set according to the map.
  550.  *   If an error occurs during calling the load_functions (like trying to load
  551.  *   a non existing node) then this function return FALSE.
  552.  */
  553. function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
  554.   $path_map = $map;
  555.   if (!_menu_load_objects($router_item, $map)) {
  556.     // An error occurred loading an object.
  557.     $router_item['access'] = FALSE;
  558.     return FALSE;
  559.   }
  560.   if ($to_arg) {
  561.     _menu_link_map_translate($path_map, $router_item['to_arg_functions']);
  562.   }
  563.  
  564.   // Generate the link path for the page request or local tasks.
  565.   $link_map = explode('/', $router_item['path']);
  566.   for ($i = 0; $i < $router_item['number_parts']; $i++) {
  567.     if ($link_map[$i] == '%') {
  568.       $link_map[$i] = $path_map[$i];
  569.     }
  570.   }
  571.   $router_item['href'] = implode('/', $link_map);
  572.   $router_item['options'] = array();
  573.   _menu_check_access($router_item, $map);
  574.  
  575.   _menu_item_localize($router_item, $map);
  576.  
  577.   return $map;
  578. }
  579.  
  580. /**
  581.  * This function translates the path elements in the map using any to_arg
  582.  * helper function. These functions take an argument and return an object.
  583.  * See http://drupal.org/node/109153 for more information.
  584.  *
  585.  * @param map
  586.  *   An array of path arguments (ex: array('node', '5'))
  587.  * @param $to_arg_functions
  588.  *   An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
  589.  */
  590. function _menu_link_map_translate(&$map, $to_arg_functions) {
  591.   if ($to_arg_functions) {
  592.     $to_arg_functions = unserialize($to_arg_functions);
  593.     foreach ($to_arg_functions as $index => $function) {
  594.       // Translate place-holders into real values.
  595.       $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
  596.       if (!empty($map[$index]) || isset($arg)) {
  597.         $map[$index] = $arg;
  598.       }
  599.       else {
  600.         unset($map[$index]);
  601.       }
  602.     }
  603.   }
  604. }
  605.  
  606. function menu_tail_to_arg($arg, $map, $index) {
  607.   return implode('/', array_slice($map, $index));
  608. }
  609.  
  610. /**
  611.  * This function is similar to _menu_translate() but does link-specific
  612.  * preparation such as always calling to_arg functions
  613.  *
  614.  * @param $item
  615.  *   A menu link
  616.  * @return
  617.  *   Returns the map of path arguments with objects loaded as defined in the
  618.  *   $item['load_functions'].
  619.  *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
  620.  *   $item['href'] is generated from link_path, possibly by to_arg functions.
  621.  *   $item['title'] is generated from link_title, and may be localized.
  622.  *   $item['options'] is unserialized; it is also changed within the call here 
  623.  *   to $item['localized_options'] by _menu_item_localize().
  624.  */
  625. function _menu_link_translate(&$item) {
  626.   $item['options'] = unserialize($item['options']);
  627.   if ($item['external']) {
  628.     $item['access'] = 1;
  629.     $map = array();
  630.     $item['href'] = $item['link_path'];
  631.     $item['title'] = $item['link_title'];
  632.     $item['localized_options'] = $item['options'];
  633.   }
  634.   else {
  635.     $map = explode('/', $item['link_path']);
  636.     _menu_link_map_translate($map, $item['to_arg_functions']);
  637.     $item['href'] = implode('/', $map);
  638.  
  639.     // Note - skip callbacks without real values for their arguments.
  640.     if (strpos($item['href'], '%') !== FALSE) {
  641.       $item['access'] = FALSE;
  642.       return FALSE;
  643.     }
  644.     // menu_tree_check_access() may set this ahead of time for links to nodes.
  645.     if (!isset($item['access'])) {
  646.       if (!_menu_load_objects($item, $map)) {
  647.         // An error occurred loading an object.
  648.         $item['access'] = FALSE;
  649.         return FALSE;
  650.       }
  651.       _menu_check_access($item, $map);
  652.     }
  653.  
  654.     _menu_item_localize($item, $map, TRUE);
  655.   }
  656.   
  657.   // Allow other customizations - e.g. adding a page-specific query string to the
  658.   // options array. For performance reasons we only invoke this hook if the link
  659.   // has the 'alter' flag set in the options array.
  660.   if (!empty($item['options']['alter'])) {
  661.     drupal_alter('translated_menu_link', $item, $map);
  662.   }
  663.   
  664.   return $map;
  665. }
  666.  
  667. /**
  668.  * Get a loaded object from a router item.
  669.  *
  670.  * menu_get_object() will provide you the current node on paths like node/5,
  671.  * node/5/revisions/48 etc. menu_get_object('user') will give you the user
  672.  * account on user/5 etc. Note - this function should never be called within a
  673.  * _to_arg function (like user_current_to_arg()) since this may result in an
  674.  * infinite recursion.
  675.  *
  676.  * @param $type
  677.  *   Type of the object. These appear in hook_menu definitons as %type. Core
  678.  *   provides aggregator_feed, aggregator_category, contact, filter_format,
  679.  *   forum_term, menu, menu_link, node, taxonomy_vocabulary, user. See the
  680.  *   relevant {$type}_load function for more on each. Defaults to node.
  681.  * @param $position
  682.  *   The expected position for $type object. For node/%node this is 1, for
  683.  *   comment/reply/%node this is 2. Defaults to 1.
  684.  * @param $path
  685.  *   See @menu_get_item for more on this. Defaults to the current path.
  686.  */
  687. function menu_get_object($type = 'node', $position = 1, $path = NULL) {
  688.   $router_item = menu_get_item($path);
  689.   if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type .'_load') {
  690.     return $router_item['map'][$position];
  691.   }
  692. }
  693.  
  694. /**
  695.  * Render a menu tree based on the current path.
  696.  *
  697.  * The tree is expanded based on the current path and dynamic paths are also
  698.  * changed according to the defined to_arg functions (for example the 'My account'
  699.  * link is changed from user/% to a link with the current user's uid).
  700.  *
  701.  * @param $menu_name
  702.  *   The name of the menu.
  703.  * @return
  704.  *   The rendered HTML of that menu on the current page.
  705.  */
  706. function menu_tree($menu_name = 'navigation') {
  707.   static $menu_output = array();
  708.  
  709.   if (!isset($menu_output[$menu_name])) {
  710.     $tree = menu_tree_page_data($menu_name);
  711.     $menu_output[$menu_name] = menu_tree_output($tree);
  712.   }
  713.   return $menu_output[$menu_name];
  714. }
  715.  
  716. /**
  717.  * Returns a rendered menu tree.
  718.  *
  719.  * @param $tree
  720.  *   A data structure representing the tree as returned from menu_tree_data.
  721.  * @return
  722.  *   The rendered HTML of that data structure.
  723.  */
  724. function menu_tree_output($tree) {
  725.   $output = '';
  726.   $items = array();
  727.  
  728.   // Pull out just the menu items we are going to render so that we
  729.   // get an accurate count for the first/last classes.
  730.   foreach ($tree as $data) {
  731.     if (!$data['link']['hidden']) {
  732.       $items[] = $data;
  733.     }
  734.   }
  735.  
  736.   $num_items = count($items);
  737.   foreach ($items as $i => $data) {
  738.     $extra_class = NULL;
  739.     if ($i == 0) {
  740.       $extra_class = 'first';
  741.     }
  742.     if ($i == $num_items - 1) {
  743.       $extra_class = 'last';
  744.     }
  745.     $link = theme('menu_item_link', $data['link']);
  746.     if ($data['below']) {
  747.       $output .= theme('menu_item', $link, $data['link']['has_children'], menu_tree_output($data['below']), $data['link']['in_active_trail'], $extra_class);
  748.     }
  749.     else {
  750.       $output .= theme('menu_item', $link, $data['link']['has_children'], '', $data['link']['in_active_trail'], $extra_class);
  751.     }
  752.   }
  753.   return $output ? theme('menu_tree', $output) : '';
  754. }
  755.  
  756. /**
  757.  * Get the data structure representing a named menu tree.
  758.  *
  759.  * Since this can be the full tree including hidden items, the data returned
  760.  * may be used for generating an an admin interface or a select.
  761.  *
  762.  * @param $menu_name
  763.  *   The named menu links to return
  764.  * @param $item
  765.  *   A fully loaded menu link, or NULL.  If a link is supplied, only the
  766.  *   path to root will be included in the returned tree- as if this link
  767.  *   represented the current page in a visible menu.
  768.  * @return
  769.  *   An tree of menu links in an array, in the order they should be rendered.
  770.  */
  771. function menu_tree_all_data($menu_name = 'navigation', $item = NULL) {
  772.   static $tree = array();
  773.  
  774.   // Use $mlid as a flag for whether the data being loaded is for the whole tree.
  775.   $mlid = isset($item['mlid']) ? $item['mlid'] : 0;
  776.   // Generate the cache ID.
  777.   $cid = 'links:'. $menu_name .':all:'. $mlid;
  778.  
  779.   if (!isset($tree[$cid])) {
  780.     // If the static variable doesn't have the data, check {cache_menu}.
  781.     $cache = cache_get($cid, 'cache_menu');
  782.     if ($cache && isset($cache->data)) {
  783.       $data = $cache->data;
  784.     }
  785.     else {
  786.       // Build and run the query, and build the tree.
  787.       if ($mlid) {
  788.         // The tree is for a single item, so we need to match the values in its
  789.         // p columns and 0 (the top level) with the plid values of other links.
  790.         $args = array(0);
  791.         for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
  792.           $args[] = $item["p$i"];
  793.         }
  794.         $args = array_unique($args);
  795.         $placeholders = implode(', ', array_fill(0, count($args), '%d'));
  796.         $where = ' AND ml.plid IN ('. $placeholders .')';
  797.         $parents = $args;
  798.         $parents[] = $item['mlid'];
  799.       }
  800.       else {
  801.         // Get all links in this menu.
  802.         $where = '';
  803.         $args = array();
  804.         $parents = array();
  805.       }
  806.       array_unshift($args, $menu_name);
  807.       // Select the links from the table, and recursively build the tree.  We
  808.       // LEFT JOIN since there is no match in {menu_router} for an external
  809.       // link.
  810.       $data['tree'] = menu_tree_data(db_query("
  811.         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.*
  812.         FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
  813.         WHERE ml.menu_name = '%s'". $where ."
  814.         ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC", $args), $parents);
  815.       $data['node_links'] = array();
  816.       menu_tree_collect_node_links($data['tree'], $data['node_links']);
  817.       // Cache the data.
  818.       cache_set($cid, $data, 'cache_menu');
  819.     }
  820.     // Check access for the current user to each item in the tree.
  821.     menu_tree_check_access($data['tree'], $data['node_links']);
  822.     $tree[$cid] = $data['tree'];
  823.   }
  824.  
  825.   return $tree[$cid];
  826. }
  827.  
  828. /**
  829.  * Get the data structure representing a named menu tree, based on the current page.
  830.  *
  831.  * The tree order is maintained by storing each parent in an individual
  832.  * field, see http://drupal.org/node/141866 for more.
  833.  *
  834.  * @param $menu_name
  835.  *   The named menu links to return
  836.  * @return
  837.  *   An array of menu links, in the order they should be rendered. The array
  838.  *   is a list of associative arrays -- these have two keys, link and below.
  839.  *   link is a menu item, ready for theming as a link. Below represents the
  840.  *   submenu below the link if there is one, and it is a subtree that has the
  841.  *   same structure described for the top-level array.
  842.  */
  843. function menu_tree_page_data($menu_name = 'navigation') {
  844.   static $tree = array();
  845.  
  846.   // Load the menu item corresponding to the current page.
  847.   if ($item = menu_get_item()) {
  848.     // Generate the cache ID.
  849.     $cid = 'links:'. $menu_name .':page:'. $item['href'] .':'. (int)$item['access'];
  850.  
  851.     if (!isset($tree[$cid])) {
  852.       // If the static variable doesn't have the data, check {cache_menu}.
  853.       $cache = cache_get($cid, 'cache_menu');
  854.       if ($cache && isset($cache->data)) {
  855.         $data = $cache->data;
  856.       }
  857.       else {
  858.         // Build and run the query, and build the tree.
  859.         if ($item['access']) {
  860.           // Check whether a menu link exists that corresponds to the current path.
  861.           $args = array($menu_name, $item['href']);
  862.           $placeholders = "'%s'";
  863.           if (drupal_is_front_page()) {
  864.             $args[] = '<front>';
  865.             $placeholders .= ", '%s'";
  866.           }
  867.           $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));
  868.  
  869.           if (empty($parents)) {
  870.             // If no link exists, we may be on a local task that's not in the links.
  871.             // TODO: Handle the case like a local task on a specific node in the menu.
  872.             $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']));
  873.           }
  874.           // We always want all the top-level links with plid == 0.
  875.           $parents[] = '0';
  876.  
  877.           // Use array_values() so that the indices are numeric for array_merge().
  878.           $args = $parents = array_unique(array_values($parents));
  879.           $placeholders = implode(', ', array_fill(0, count($args), '%d'));
  880.           $expanded = variable_get('menu_expanded', array());
  881.           // Check whether the current menu has any links set to be expanded.
  882.           if (in_array($menu_name, $expanded)) {
  883.             // Collect all the links set to be expanded, and then add all of
  884.             // their children to the list as well.
  885.             do {
  886.               $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));
  887.               $num_rows = FALSE;
  888.               while ($item = db_fetch_array($result)) {
  889.                 $args[] = $item['mlid'];
  890.                 $num_rows = TRUE;
  891.               }
  892.               $placeholders = implode(', ', array_fill(0, count($args), '%d'));
  893.             } while ($num_rows);
  894.           }
  895.           array_unshift($args, $menu_name);
  896.         }
  897.         else {
  898.           // Show only the top-level menu items when access is denied.
  899.           $args = array($menu_name, '0');
  900.           $placeholders = '%d';
  901.           $parents = array();
  902.         }
  903.         // Select the links from the table, and recursively build the tree. We
  904.         // LEFT JOIN since there is no match in {menu_router} for an external
  905.         // link.
  906.         $data['tree'] = menu_tree_data(db_query("
  907.           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.*
  908.           FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
  909.           WHERE ml.menu_name = '%s' AND ml.plid IN (". $placeholders .")
  910.           ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC", $args), $parents);
  911.         $data['node_links'] = array();
  912.         menu_tree_collect_node_links($data['tree'], $data['node_links']);
  913.         // Cache the data.
  914.         cache_set($cid, $data, 'cache_menu');
  915.       }
  916.       // Check access for the current user to each item in the tree.
  917.       menu_tree_check_access($data['tree'], $data['node_links']);
  918.       $tree[$cid] = $data['tree'];
  919.     }
  920.     return $tree[$cid];
  921.   }
  922.  
  923.   return array();
  924. }
  925.  
  926. /**
  927.  * Recursive helper function - collect node links.
  928.  */
  929. function menu_tree_collect_node_links(&$tree, &$node_links) {
  930.   foreach ($tree as $key => $v) {
  931.     if ($tree[$key]['link']['router_path'] == 'node/%') {
  932.       $nid = substr($tree[$key]['link']['link_path'], 5);
  933.       if (is_numeric($nid)) {
  934.         $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link'];
  935.         $tree[$key]['link']['access'] = FALSE;
  936.       }
  937.     }
  938.     if ($tree[$key]['below']) {
  939.       menu_tree_collect_node_links($tree[$key]['below'], $node_links);
  940.     }
  941.   }
  942. }
  943.  
  944. /**
  945.  * Check access and perform other dynamic operations for each link in the tree.
  946.  */
  947. function menu_tree_check_access(&$tree, $node_links = array()) {
  948.  
  949.   if ($node_links) {
  950.     // Use db_rewrite_sql to evaluate view access without loading each full node.
  951.     $nids = array_keys($node_links);
  952.     $placeholders = '%d'. str_repeat(', %d', count($nids) - 1);
  953.     $result = db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.status = 1 AND n.nid IN (". $placeholders .")"), $nids);
  954.     while ($node = db_fetch_array($result)) {
  955.       $nid = $node['nid'];
  956.       foreach ($node_links[$nid] as $mlid => $link) {
  957.         $node_links[$nid][$mlid]['access'] = TRUE;
  958.       }
  959.     }
  960.   }
  961.   _menu_tree_check_access($tree);
  962.   return;
  963. }
  964.  
  965. /**
  966.  * Recursive helper function for menu_tree_check_access()
  967.  */
  968. function _menu_tree_check_access(&$tree) {
  969.   $new_tree = array();
  970.   foreach ($tree as $key => $v) {
  971.     $item = &$tree[$key]['link'];
  972.     _menu_link_translate($item);
  973.     if ($item['access']) {
  974.       if ($tree[$key]['below']) {
  975.         _menu_tree_check_access($tree[$key]['below']);
  976.       }
  977.       // The weights are made a uniform 5 digits by adding 50000 as an offset.
  978.       // After _menu_link_translate(), $item['title'] has the localized link title.
  979.       // Adding the mlid to the end of the index insures that it is unique.
  980.       $new_tree[(50000 + $item['weight']) .' '. $item['title'] .' '. $item['mlid']] = $tree[$key];
  981.     }
  982.   }
  983.   // Sort siblings in the tree based on the weights and localized titles.
  984.   ksort($new_tree);
  985.   $tree = $new_tree;
  986. }
  987.  
  988. /**
  989.  * Build the data representing a menu tree.
  990.  *
  991.  * @param $result
  992.  *   The database result.
  993.  * @param $parents
  994.  *   An array of the plid values that represent the path from the current page
  995.  *   to the root of the menu tree.
  996.  * @param $depth
  997.  *   The depth of the current menu tree.
  998.  * @return
  999.  *   See menu_tree_page_data for a description of the data structure.
  1000.  */
  1001. function menu_tree_data($result = NULL, $parents = array(), $depth = 1) {
  1002.   list(, $tree) = _menu_tree_data($result, $parents, $depth);
  1003.   return $tree;
  1004. }
  1005.  
  1006. /**
  1007.  * Recursive helper function to build the data representing a menu tree.
  1008.  *
  1009.  * The function is a bit complex because the rendering of an item depends on
  1010.  * the next menu item. So we are always rendering the element previously
  1011.  * processed not the current one.
  1012.  */
  1013. function _menu_tree_data($result, $parents, $depth, $previous_element = '') {
  1014.   $remnant = NULL;
  1015.   $tree = array();
  1016.   while ($item = db_fetch_array($result)) {
  1017.     // We need to determine if we're on the path to root so we can later build
  1018.     // the correct active trail and breadcrumb.
  1019.     $item['in_active_trail'] = in_array($item['mlid'], $parents);
  1020.     // The current item is the first in a new submenu.
  1021.     if ($item['depth'] > $depth) {
  1022.       // _menu_tree returns an item and the menu tree structure.
  1023.       list($item, $below) = _menu_tree_data($result, $parents, $item['depth'], $item);
  1024.       if ($previous_element) {
  1025.         $tree[$previous_element['mlid']] = array(
  1026.           'link' => $previous_element,
  1027.           'below' => $below,
  1028.         );
  1029.       }
  1030.       else {
  1031.         $tree = $below;
  1032.       }
  1033.       // We need to fall back one level.
  1034.       if (!isset($item) || $item['depth'] < $depth) {
  1035.         return array($item, $tree);
  1036.       }
  1037.       // This will be the link to be output in the next iteration.
  1038.       $previous_element = $item;
  1039.     }
  1040.     // We are at the same depth, so we use the previous element.
  1041.     elseif ($item['depth'] == $depth) {
  1042.       if ($previous_element) {
  1043.         // Only the first time.
  1044.         $tree[$previous_element['mlid']] = array(
  1045.           'link' => $previous_element,
  1046.           'below' => FALSE,
  1047.         );
  1048.       }
  1049.       // This will be the link to be output in the next iteration.
  1050.       $previous_element = $item;
  1051.     }
  1052.     // The submenu ended with the previous item, so pass back the current item.
  1053.     else {
  1054.       $remnant = $item;
  1055.       break;
  1056.     }
  1057.   }
  1058.   if ($previous_element) {
  1059.     // We have one more link dangling.
  1060.     $tree[$previous_element['mlid']] = array(
  1061.       'link' => $previous_element,
  1062.       'below' => FALSE,
  1063.     );
  1064.   }
  1065.   return array($remnant, $tree);
  1066. }
  1067.  
  1068. /**
  1069.  * Generate the HTML output for a single menu link.
  1070.  *
  1071.  * @ingroup themeable
  1072.  */
  1073. function theme_menu_item_link($link) {
  1074.   if (empty($link['localized_options'])) {
  1075.     $link['localized_options'] = array();
  1076.   }
  1077.  
  1078.   return l($link['title'], $link['href'], $link['localized_options']);
  1079. }
  1080.  
  1081. /**
  1082.  * Generate the HTML output for a menu tree
  1083.  *
  1084.  * @ingroup themeable
  1085.  */
  1086. function theme_menu_tree($tree) {
  1087.   return '<ul class="menu">'. $tree .'</ul>';
  1088. }
  1089.  
  1090. /**
  1091.  * Generate the HTML output for a menu item and submenu.
  1092.  *
  1093.  * @ingroup themeable
  1094.  */
  1095. function theme_menu_item($link, $has_children, $menu = '', $in_active_trail = FALSE, $extra_class = NULL) {
  1096.   $class = ($menu ? 'expanded' : ($has_children ? 'collapsed' : 'leaf'));
  1097.   if (!empty($extra_class)) {
  1098.     $class .= ' '. $extra_class;
  1099.   }
  1100.   if ($in_active_trail) {
  1101.     $class .= ' active-trail';
  1102.   }
  1103.   return '<li class="'. $class .'">'. $link . $menu ."</li>\n";
  1104. }
  1105.  
  1106. /**
  1107.  * Generate the HTML output for a single local task link.
  1108.  *
  1109.  * @ingroup themeable
  1110.  */
  1111. function theme_menu_local_task($link, $active = FALSE) {
  1112.   return '<li '. ($active ? 'class="active" ' : '') .'>'. $link ."</li>\n";
  1113. }
  1114.  
  1115. /**
  1116.  * Generates elements for the $arg array in the help hook.
  1117.  */
  1118. function drupal_help_arg($arg = array()) {
  1119.   // Note - the number of empty elements should be > MENU_MAX_PARTS.
  1120.   return $arg + array('', '', '', '', '', '', '', '', '', '', '', '');
  1121. }
  1122.  
  1123. /**
  1124.  * Returns the help associated with the active menu item.
  1125.  */
  1126. function menu_get_active_help() {
  1127.   $output = '';
  1128.   $router_path = menu_tab_root_path();
  1129.  
  1130.   $arg = drupal_help_arg(arg(NULL));
  1131.   $empty_arg = drupal_help_arg();
  1132.  
  1133.   foreach (module_list() as $name) {
  1134.     if (module_hook($name, 'help')) {
  1135.       // Lookup help for this path.
  1136.       if ($help = module_invoke($name, 'help', $router_path, $arg)) {
  1137.         $output .= $help ."\n";
  1138.       }
  1139.       // Add "more help" link on admin pages if the module provides a
  1140.       // standalone help page.
  1141.       if ($arg[0] == "admin" && module_exists('help') && module_invoke($name, 'help', 'admin/help#'. $arg[2], $empty_arg) && $help) {
  1142.         $output .= theme("more_help_link", url('admin/help/'. $arg[2]));
  1143.       }
  1144.     }
  1145.   }
  1146.   return $output;
  1147. }
  1148.  
  1149. /**
  1150.  * Build a list of named menus.
  1151.  */
  1152. function menu_get_names($reset = FALSE) {
  1153.   static $names;
  1154.  
  1155.   if ($reset || empty($names)) {
  1156.     $names = array();
  1157.     $result = db_query("SELECT DISTINCT(menu_name) FROM {menu_links} ORDER BY menu_name");
  1158.     while ($name = db_fetch_array($result)) {
  1159.       $names[] = $name['menu_name'];
  1160.     }
  1161.   }
  1162.   return $names;
  1163. }
  1164.  
  1165. /**
  1166.  * Return an array containing the names of system-defined (default) menus.
  1167.  */
  1168. function menu_list_system_menus() {
  1169.   return array('navigation', 'primary-links', 'secondary-links');
  1170. }
  1171.  
  1172. /**
  1173.  * Return an array of links to be rendered as the Primary links.
  1174.  */
  1175. function menu_primary_links() {
  1176.   return menu_navigation_links(variable_get('menu_primary_links_source', 'primary-links'));
  1177. }
  1178.  
  1179. /**
  1180.  * Return an array of links to be rendered as the Secondary links.
  1181.  */
  1182. function menu_secondary_links() {
  1183.  
  1184.   // If the secondary menu source is set as the primary menu, we display the
  1185.   // second level of the primary menu.
  1186.   if (variable_get('menu_secondary_links_source', 'secondary-links') == variable_get('menu_primary_links_source', 'primary-links')) {
  1187.     return menu_navigation_links(variable_get('menu_primary_links_source', 'primary-links'), 1);
  1188.   }
  1189.   else {
  1190.     return menu_navigation_links(variable_get('menu_secondary_links_source', 'secondary-links'), 0);
  1191.   }
  1192. }
  1193.  
  1194. /**
  1195.  * Return an array of links for a navigation menu.
  1196.  *
  1197.  * @param $menu_name
  1198.  *   The name of the menu.
  1199.  * @param $level
  1200.  *   Optional, the depth of the menu to be returned.
  1201.  * @return
  1202.  *   An array of links of the specified menu and level.
  1203.  */
  1204. function menu_navigation_links($menu_name, $level = 0) {
  1205.   // Don't even bother querying the menu table if no menu is specified.
  1206.   if (empty($menu_name)) {
  1207.     return array();
  1208.   }
  1209.  
  1210.   // Get the menu hierarchy for the current page.
  1211.   $tree = menu_tree_page_data($menu_name);
  1212.  
  1213.   // Go down the active trail until the right level is reached.
  1214.   while ($level-- > 0 && $tree) {
  1215.     // Loop through the current level's items until we find one that is in trail.
  1216.     while ($item = array_shift($tree)) {
  1217.       if ($item['link']['in_active_trail']) {
  1218.         // If the item is in the active trail, we continue in the subtree.
  1219.         $tree = empty($item['below']) ? array() : $item['below'];
  1220.         break;
  1221.       }
  1222.     }
  1223.   }
  1224.  
  1225.   // Create a single level of links.
  1226.   $links = array();
  1227.   foreach ($tree as $item) {
  1228.     if (!$item['link']['hidden']) {
  1229.       $l = $item['link']['localized_options'];
  1230.       $l['href'] = $item['link']['href'];
  1231.       $l['title'] = $item['link']['title'];
  1232.       // Keyed with unique menu id to generate classes from theme_links().
  1233.       $links['menu-'. $item['link']['mlid']] = $l;
  1234.     }
  1235.   }
  1236.   return $links;
  1237. }
  1238.  
  1239. /**
  1240.  * Collects the local tasks (tabs) for a given level.
  1241.  *
  1242.  * @param $level
  1243.  *   The level of tasks you ask for. Primary tasks are 0, secondary are 1.
  1244.  * @param $return_root
  1245.  *   Whether to return the root path for the current page.
  1246.  * @return
  1247.  *   Themed output corresponding to the tabs of the requested level, or
  1248.  *   router path if $return_root == TRUE. This router path corresponds to
  1249.  *   a parent tab, if the current page is a default local task.
  1250.  */
  1251. function menu_local_tasks($level = 0, $return_root = FALSE) {
  1252.   static $tabs;
  1253.   static $root_path;
  1254.  
  1255.   if (!isset($tabs)) {
  1256.     $tabs = array();
  1257.  
  1258.     $router_item = menu_get_item();
  1259.     if (!$router_item || !$router_item['access']) {
  1260.       return '';
  1261.     }
  1262.     // Get all tabs and the root page.
  1263.     $result = db_query("SELECT * FROM {menu_router} WHERE tab_root = '%s' ORDER BY weight, title", $router_item['tab_root']);
  1264.     $map = arg();
  1265.     $children = array();
  1266.     $tasks = array();
  1267.     $root_path = $router_item['path'];
  1268.  
  1269.     while ($item = db_fetch_array($result)) {
  1270.       _menu_translate($item, $map, TRUE);
  1271.       if ($item['tab_parent']) {
  1272.         // All tabs, but not the root page.
  1273.         $children[$item['tab_parent']][$item['path']] = $item;
  1274.       }
  1275.       // Store the translated item for later use.
  1276.       $tasks[$item['path']] = $item;
  1277.     }
  1278.  
  1279.     // Find all tabs below the current path.
  1280.     $path = $router_item['path'];
  1281.     // Tab parenting may skip levels, so the number of parts in the path may not
  1282.     // equal the depth. Thus we use the $depth counter (offset by 1000 for ksort).
  1283.     $depth = 1001;
  1284.     while (isset($children[$path])) {
  1285.       $tabs_current = '';
  1286.       $next_path = '';
  1287.       $count = 0;
  1288.       foreach ($children[$path] as $item) {
  1289.         if ($item['access']) {
  1290.           $count++;
  1291.           // The default task is always active.
  1292.           if ($item['type'] == MENU_DEFAULT_LOCAL_TASK) {
  1293.             // Find the first parent which is not a default local task.
  1294.             for ($p = $item['tab_parent']; $tasks[$p]['type'] == MENU_DEFAULT_LOCAL_TASK; $p = $tasks[$p]['tab_parent']);
  1295.             $link = theme('menu_item_link', array('href' => $tasks[$p]['href']) + $item);
  1296.             $tabs_current .= theme('menu_local_task', $link, TRUE);
  1297.             $next_path = $item['path'];
  1298.           }
  1299.           else {
  1300.             $link = theme('menu_item_link', $item);
  1301.             $tabs_current .= theme('menu_local_task', $link);
  1302.           }
  1303.         }
  1304.       }
  1305.       $path = $next_path;
  1306.       $tabs[$depth]['count'] = $count;
  1307.       $tabs[$depth]['output'] = $tabs_current;
  1308.       $depth++;
  1309.     }
  1310.  
  1311.     // Find all tabs at the same level or above the current one.
  1312.     $parent = $router_item['tab_parent'];
  1313.     $path = $router_item['path'];
  1314.     $current = $router_item;
  1315.     $depth = 1000;
  1316.     while (isset($children[$parent])) {
  1317.       $tabs_current = '';
  1318.       $next_path = '';
  1319.       $next_parent = '';
  1320.       $count = 0;
  1321.       foreach ($children[$parent] as $item) {
  1322.         if ($item['access']) {
  1323.           $count++;
  1324.           if ($item['type'] == MENU_DEFAULT_LOCAL_TASK) {
  1325.             // Find the first parent which is not a default local task.
  1326.             for ($p = $item['tab_parent']; $tasks[$p]['type'] == MENU_DEFAULT_LOCAL_TASK; $p = $tasks[$p]['tab_parent']);
  1327.             $link = theme('menu_item_link', array('href' => $tasks[$p]['href']) + $item);
  1328.             if ($item['path'] == $router_item['path']) {
  1329.               $root_path = $tasks[$p]['path'];
  1330.             }
  1331.           }
  1332.           else {
  1333.             $link = theme('menu_item_link', $item);
  1334.           }
  1335.           // We check for the active tab.
  1336.           if ($item['path'] == $path) {
  1337.             $tabs_current .= theme('menu_local_task', $link, TRUE);
  1338.             $next_path = $item['tab_parent'];
  1339.             if (isset($tasks[$next_path])) {
  1340.               $next_parent = $tasks[$next_path]['tab_parent'];
  1341.             }
  1342.           }
  1343.           else {
  1344.             $tabs_current .= theme('menu_local_task', $link);
  1345.           }
  1346.         }
  1347.       }
  1348.       $path = $next_path;
  1349.       $parent = $next_parent;
  1350.       $tabs[$depth]['count'] = $count;
  1351.       $tabs[$depth]['output'] = $tabs_current;
  1352.       $depth--;
  1353.     }
  1354.     // Sort by depth.
  1355.     ksort($tabs);
  1356.     // Remove the depth, we are interested only in their relative placement.
  1357.     $tabs = array_values($tabs);
  1358.   }
  1359.  
  1360.   if ($return_root) {
  1361.     return $root_path;
  1362.   }
  1363.   else {
  1364.     // We do not display single tabs.
  1365.     return (isset($tabs[$level]) && $tabs[$level]['count'] > 1) ? $tabs[$level]['output'] : '';
  1366.   }
  1367. }
  1368.  
  1369. /**
  1370.  * Returns the rendered local tasks at the top level.
  1371.  */
  1372. function menu_primary_local_tasks() {
  1373.   return menu_local_tasks(0);
  1374. }
  1375.  
  1376. /**
  1377.  * Returns the rendered local tasks at the second level.
  1378.  */
  1379. function menu_secondary_local_tasks() {
  1380.   return menu_local_tasks(1);
  1381. }
  1382.  
  1383. /**
  1384.  * Returns the router path, or the path of the parent tab of a default local task.
  1385.  */
  1386. function menu_tab_root_path() {
  1387.   return menu_local_tasks(0, TRUE);
  1388. }
  1389.  
  1390. /**
  1391.  * Returns the rendered local tasks. The default implementation renders them as tabs.
  1392.  *
  1393.  * @ingroup themeable
  1394.  */
  1395. function theme_menu_local_tasks() {
  1396.   $output = '';
  1397.  
  1398.   if ($primary = menu_primary_local_tasks()) {
  1399.     $output .= "<ul class=\"tabs primary\">\n". $primary ."</ul>\n";
  1400.   }
  1401.   if ($secondary = menu_secondary_local_tasks()) {
  1402.     $output .= "<ul class=\"tabs secondary\">\n". $secondary ."</ul>\n";
  1403.   }
  1404.  
  1405.   return $output;
  1406. }
  1407.  
  1408. /**
  1409.  * Set (or get) the active menu for the current page - determines the active trail.
  1410.  */
  1411. function menu_set_active_menu_name($menu_name = NULL) {
  1412.   static $active;
  1413.  
  1414.   if (isset($menu_name)) {
  1415.     $active = $menu_name;
  1416.   }
  1417.   elseif (!isset($active)) {
  1418.     $active = 'navigation';
  1419.   }
  1420.   return $active;
  1421. }
  1422.  
  1423. /**
  1424.  * Get the active menu for the current page - determines the active trail.
  1425.  */
  1426. function menu_get_active_menu_name() {
  1427.   return menu_set_active_menu_name();
  1428. }
  1429.  
  1430. /**
  1431.  * Set the active path, which determines which page is loaded.
  1432.  *
  1433.  * @param $path
  1434.  *   A Drupal path - not a path alias.
  1435.  *
  1436.  * Note that this may not have the desired effect unless invoked very early
  1437.  * in the page load, such as during hook_boot, or unless you call
  1438.  * menu_execute_active_handler() to generate your page output.
  1439.  */
  1440. function menu_set_active_item($path) {
  1441.   $_GET['q'] = $path;
  1442. }
  1443.  
  1444. /**
  1445.  * Set (or get) the active trail for the current page - the path to root in the menu tree.
  1446.  */
  1447. function menu_set_active_trail($new_trail = NULL) {
  1448.   static $trail;
  1449.  
  1450.   if (isset($new_trail)) {
  1451.     $trail = $new_trail;
  1452.   }
  1453.   elseif (!isset($trail)) {
  1454.     $trail = array();
  1455.     $trail[] = array('title' => t('Home'), 'href' => '<front>', 'localized_options' => array(), 'type' => 0);
  1456.     $item = menu_get_item();
  1457.  
  1458.     // Check whether the current item is a local task (displayed as a tab).
  1459.     if ($item['tab_parent']) {
  1460.       // The title of a local task is used for the tab, never the page title.
  1461.       // Thus, replace it with the item corresponding to the root path to get
  1462.       // the relevant href and title.  For example, the menu item corresponding
  1463.       // to 'admin' is used when on the 'By module' tab at 'admin/by-module'.
  1464.       $parts = explode('/', $item['tab_root']);
  1465.       $args = arg();
  1466.       // Replace wildcards in the root path using the current path.
  1467.       foreach ($parts as $index => $part) {
  1468.         if ($part == '%') {
  1469.           $parts[$index] = $args[$index];
  1470.         }
  1471.       }
  1472.       // Retrieve the menu item using the root path after wildcard replacement.
  1473.       $root_item = menu_get_item(implode('/', $parts));
  1474.       if ($root_item && $root_item['access']) {
  1475.         $item = $root_item;
  1476.       }
  1477.     }
  1478.  
  1479.     $tree = menu_tree_page_data(menu_get_active_menu_name());
  1480.     list($key, $curr) = each($tree);
  1481.  
  1482.     while ($curr) {
  1483.       // Terminate the loop when we find the current path in the active trail.
  1484.       if ($curr['link']['href'] == $item['href']) {
  1485.         $trail[] = $curr['link'];
  1486.         $curr = FALSE;
  1487.       }
  1488.       else {
  1489.         // Move to the child link if it's in the active trail.
  1490.         if ($curr['below'] && $curr['link']['in_active_trail']) {
  1491.           $trail[] = $curr['link'];
  1492.           $tree = $curr['below'];
  1493.         }
  1494.         list($key, $curr) = each($tree);
  1495.       }
  1496.     }
  1497.     // Make sure the current page is in the trail (needed for the page title),
  1498.     // but exclude tabs and the front page.
  1499.     $last = count($trail) - 1;
  1500.     if ($trail[$last]['href'] != $item['href'] && !(bool)($item['type'] & MENU_IS_LOCAL_TASK) && !drupal_is_front_page()) {
  1501.       $trail[] = $item;
  1502.     }
  1503.   }
  1504.   return $trail;
  1505. }
  1506.  
  1507. /**
  1508.  * Get the active trail for the current page - the path to root in the menu tree.
  1509.  */
  1510. function menu_get_active_trail() {
  1511.   return menu_set_active_trail();
  1512. }
  1513.  
  1514. /**
  1515.  * Get the breadcrumb for the current page, as determined by the active trail.
  1516.  */
  1517. function menu_get_active_breadcrumb() {
  1518.   $breadcrumb = array();
  1519.   
  1520.   // No breadcrumb for the front page.
  1521.   if (drupal_is_front_page()) {
  1522.     return $breadcrumb;
  1523.   }
  1524.  
  1525.   $item = menu_get_item();
  1526.   if ($item && $item['access']) {
  1527.     $active_trail = menu_get_active_trail();
  1528.  
  1529.     foreach ($active_trail as $parent) {
  1530.       $breadcrumb[] = l($parent['title'], $parent['href'], $parent['localized_options']);
  1531.     }
  1532.     $end = end($active_trail);
  1533.  
  1534.     // Don't show a link to the current page in the breadcrumb trail.
  1535.     if ($item['href'] == $end['href'] || ($item['type'] == MENU_DEFAULT_LOCAL_TASK && $end['href'] != '<front>')) {
  1536.       array_pop($breadcrumb);
  1537.     }
  1538.   }
  1539.   return $breadcrumb;
  1540. }
  1541.  
  1542. /**
  1543.  * Get the title of the current page, as determined by the active trail.
  1544.  */
  1545. function menu_get_active_title() {
  1546.   $active_trail = menu_get_active_trail();
  1547.  
  1548.   foreach (array_reverse($active_trail) as $item) {
  1549.     if (!(bool)($item['type'] & MENU_IS_LOCAL_TASK)) {
  1550.       return $item['title'];
  1551.     }
  1552.   }
  1553. }
  1554.  
  1555. /**
  1556.  * Get a menu link by its mlid, access checked and link translated for rendering.
  1557.  *
  1558.  * This function should never be called from within node_load() or any other
  1559.  * function used as a menu object load function since an infinite recursion may
  1560.  * occur.
  1561.  *
  1562.  * @param $mlid
  1563.  *   The mlid of the menu item.
  1564.  * @return
  1565.  *   A menu link, with $item['access'] filled and link translated for
  1566.  *   rendering.
  1567.  */
  1568. function menu_link_load($mlid) {
  1569.   if (is_numeric($mlid) && $item = db_fetch_array(db_query("SELECT m.*, ml.* FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE ml.mlid = %d", $mlid))) {
  1570.     _menu_link_translate($item);
  1571.     return $item;
  1572.   }
  1573.   return FALSE;
  1574. }
  1575.  
  1576. /**
  1577.  * Clears the cached cached data for a single named menu.
  1578.  */
  1579. function menu_cache_clear($menu_name = 'navigation') {
  1580.   static $cache_cleared = array();
  1581.  
  1582.   if (empty($cache_cleared[$menu_name])) {
  1583.     cache_clear_all('links:'. $menu_name .':', 'cache_menu', TRUE);
  1584.     $cache_cleared[$menu_name] = 1;
  1585.   }
  1586.   elseif ($cache_cleared[$menu_name] == 1) {
  1587.     register_shutdown_function('cache_clear_all', 'links:'. $menu_name .':', 'cache_menu', TRUE);
  1588.     $cache_cleared[$menu_name] = 2;
  1589.   }
  1590. }
  1591.  
  1592. /**
  1593.  * Clears all cached menu data.  This should be called any time broad changes
  1594.  * might have been made to the router items or menu links.
  1595.  */
  1596. function menu_cache_clear_all() {
  1597.   cache_clear_all('*', 'cache_menu', TRUE);
  1598. }
  1599.  
  1600. /**
  1601.  * (Re)populate the database tables used by various menu functions.
  1602.  *
  1603.  * This function will clear and populate the {menu_router} table, add entries
  1604.  * to {menu_links} for new router items, then remove stale items from
  1605.  * {menu_links}. If called from update.php or install.php, it will also
  1606.  * schedule a call to itself on the first real page load from
  1607.  * menu_execute_active_handler(), because the maintenance page environment
  1608.  * is different and leaves stale data in the menu tables.
  1609.  */
  1610. function menu_rebuild() {
  1611.   variable_del('menu_rebuild_needed');
  1612.   menu_cache_clear_all();
  1613.   $menu = menu_router_build(TRUE);
  1614.   _menu_navigation_links_rebuild($menu);
  1615.   // Clear the page and block caches.
  1616.   _menu_clear_page_cache();
  1617.   if (defined('MAINTENANCE_MODE')) {
  1618.     variable_set('menu_rebuild_needed', TRUE);
  1619.   }
  1620. }
  1621.  
  1622. /**
  1623.  * Collect, alter and store the menu definitions.
  1624.  */
  1625. function menu_router_build($reset = FALSE) {
  1626.   static $menu;
  1627.  
  1628.   if (!isset($menu) || $reset) {
  1629.     if (!$reset && ($cache = cache_get('router:', 'cache_menu')) && isset($cache->data)) {
  1630.       $menu = $cache->data;
  1631.     }
  1632.     else {
  1633.       db_query('DELETE FROM {menu_router}');
  1634.       // We need to manually call each module so that we can know which module
  1635.       // a given item came from.
  1636.       $callbacks = array();
  1637.       foreach (module_implements('menu') as $module) {
  1638.         $router_items = call_user_func($module .'_menu');
  1639.         if (isset($router_items) && is_array($router_items)) {
  1640.           foreach (array_keys($router_items) as $path) {
  1641.             $router_items[$path]['module'] = $module;
  1642.           }
  1643.           $callbacks = array_merge($callbacks, $router_items);
  1644.         }
  1645.       }
  1646.       // Alter the menu as defined in modules, keys are like user/%user.
  1647.       drupal_alter('menu', $callbacks);
  1648.       $menu = _menu_router_build($callbacks);
  1649.       cache_set('router:', $menu, 'cache_menu');
  1650.     }
  1651.   }
  1652.   return $menu;
  1653. }
  1654.  
  1655. /**
  1656.  * Builds a link from a router item.
  1657.  */
  1658. function _menu_link_build($item) {
  1659.   if ($item['type'] == MENU_CALLBACK) {
  1660.     $item['hidden'] = -1;
  1661.   }
  1662.   elseif ($item['type'] == MENU_SUGGESTED_ITEM) {
  1663.     $item['hidden'] = 1;
  1664.   }
  1665.   // Note, we set this as 'system', so that we can be sure to distinguish all
  1666.   // the menu links generated automatically from entries in {menu_router}.
  1667.   $item['module'] = 'system';
  1668.   $item += array(
  1669.     'menu_name' => 'navigation',
  1670.     'link_title' => $item['title'],
  1671.     'link_path' => $item['path'],
  1672.     'hidden' => 0,
  1673.     'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])),
  1674.   );
  1675.   return $item;
  1676. }
  1677.  
  1678. /**
  1679.  * Helper function to build menu links for the items in the menu router.
  1680.  */
  1681. function _menu_navigation_links_rebuild($menu) {
  1682.   // Add normal and suggested items as links.
  1683.   $menu_links = array();
  1684.   foreach ($menu as $path => $item) {
  1685.     if ($item['_visible']) {
  1686.       $item = _menu_link_build($item);
  1687.       $menu_links[$path] = $item;
  1688.       $sort[$path] = $item['_number_parts'];
  1689.     }
  1690.   }
  1691.   if ($menu_links) {
  1692.     // Make sure no child comes before its parent.
  1693.     array_multisort($sort, SORT_NUMERIC, $menu_links);
  1694.  
  1695.     foreach ($menu_links as $item) {
  1696.       $existing_item = db_fetch_array(db_query("SELECT mlid, menu_name, plid, customized, has_children, updated FROM {menu_links} WHERE link_path = '%s' AND module = '%s'", $item['link_path'], 'system'));
  1697.       if ($existing_item) {
  1698.         $item['mlid'] = $existing_item['mlid'];
  1699.         $item['menu_name'] = $existing_item['menu_name'];
  1700.         $item['plid'] = $existing_item['plid'];
  1701.         $item['has_children'] = $existing_item['has_children'];
  1702.         $item['updated'] = $existing_item['updated'];
  1703.       }
  1704.       if (!$existing_item || !$existing_item['customized']) {
  1705.         menu_link_save($item);
  1706.       }
  1707.     }
  1708.   }
  1709.   $placeholders = db_placeholders($menu, 'varchar');
  1710.   $paths = array_keys($menu);
  1711.   // Updated items and customized items which router paths are gone need new
  1712.   // router paths.
  1713.   $result = db_query("SELECT ml.link_path, ml.mlid, ml.router_path, ml.updated FROM {menu_links} ml WHERE ml.updated = 1 OR (router_path NOT IN ($placeholders) AND external = 0 AND customized = 1)", $paths);
  1714.   while ($item = db_fetch_array($result)) {
  1715.     $router_path = _menu_find_router_path($menu, $item['link_path']);
  1716.     if (!empty($router_path) && ($router_path != $item['router_path'] || $item['updated'])) {
  1717.       // If the router path and the link path matches, it's surely a working
  1718.       // item, so we clear the updated flag.
  1719.       $updated = $item['updated'] && $router_path != $item['link_path'];
  1720.       db_query("UPDATE {menu_links} SET router_path = '%s', updated = %d WHERE mlid = %d", $router_path, $updated, $item['mlid']);
  1721.     }
  1722.   }
  1723.   // Find any items where their router path does not exist any more.
  1724.   $result = db_query("SELECT * FROM {menu_links} WHERE router_path NOT IN ($placeholders) AND external = 0 AND updated = 0 AND customized = 0 ORDER BY depth DESC", $paths);
  1725.   // Remove all such items. Starting from those with the greatest depth will
  1726.   // minimize the amount of re-parenting done by menu_link_delete().
  1727.   while ($item = db_fetch_array($result)) {
  1728.     _menu_delete_item($item, TRUE);
  1729.   }
  1730. }
  1731.  
  1732. /**
  1733.  * Delete one or several menu links.
  1734.  *
  1735.  * @param $mlid
  1736.  *   A valid menu link mlid or NULL. If NULL, $path is used.
  1737.  * @param $path
  1738.  *   The path to the menu items to be deleted. $mlid must be NULL.
  1739.  */
  1740. function menu_link_delete($mlid, $path = NULL) {
  1741.   if (isset($mlid)) {
  1742.     _menu_delete_item(db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $mlid)));
  1743.   }
  1744.   else {
  1745.     $result = db_query("SELECT * FROM {menu_links} WHERE link_path = '%s'", $path);
  1746.     while ($link = db_fetch_array($result)) {
  1747.       _menu_delete_item($link);
  1748.     }
  1749.   }
  1750. }
  1751.  
  1752. /**
  1753.  * Helper function for menu_link_delete; deletes a single menu link.
  1754.  *
  1755.  * @param $item
  1756.  *   Item to be deleted.
  1757.  * @param $force
  1758.  *   Forces deletion. Internal use only, setting to TRUE is discouraged.
  1759.  */
  1760. function _menu_delete_item($item, $force = FALSE) {
  1761.   if ($item && ($item['module'] != 'system' || $item['updated'] || $force)) {
  1762.     // Children get re-attached to the item's parent.
  1763.     if ($item['has_children']) {
  1764.       $result = db_query("SELECT mlid FROM {menu_links} WHERE plid = %d", $item['mlid']);
  1765.       while ($m = db_fetch_array($result)) {
  1766.         $child = menu_link_load($m['mlid']);
  1767.         $child['plid'] = $item['plid'];
  1768.         menu_link_save($child);
  1769.       }
  1770.     }
  1771.     db_query('DELETE FROM {menu_links} WHERE mlid = %d', $item['mlid']);
  1772.  
  1773.     // Update the has_children status of the parent.
  1774.     _menu_update_parental_status($item);
  1775.     menu_cache_clear($item['menu_name']);
  1776.     _menu_clear_page_cache();
  1777.   }
  1778. }
  1779.  
  1780. /**
  1781.  * Save a menu link.
  1782.  *
  1783.  * @param $item
  1784.  *   An array representing a menu link item. The only mandatory keys are
  1785.  *   link_path and link_title. Possible keys are
  1786.  *     menu_name   default is navigation
  1787.  *     weight      default is 0
  1788.  *     expanded    whether the item is expanded.
  1789.  *     options     An array of options, @see l for more.
  1790.  *     mlid        Set to an existing value, or 0 or NULL to insert a new link.
  1791.  *     plid        The mlid of the parent.
  1792.  *     router_path The path of the relevant router item.
  1793.  */
  1794. function menu_link_save(&$item) {
  1795.   $menu = menu_router_build();
  1796.  
  1797.   drupal_alter('menu_link', $item, $menu);
  1798.  
  1799.   // This is the easiest way to handle the unique internal path '<front>',
  1800.   // since a path marked as external does not need to match a router path.
  1801.   $item['_external'] = menu_path_is_external($item['link_path'])  || $item['link_path'] == '<front>';
  1802.   // Load defaults.
  1803.   $item += array(
  1804.     'menu_name' => 'navigation',
  1805.     'weight' => 0,
  1806.     'link_title' => '',
  1807.     'hidden' => 0,
  1808.     'has_children' => 0,
  1809.     'expanded' => 0,
  1810.     'options' => array(),
  1811.     'module' => 'menu',
  1812.     'customized' => 0,
  1813.     'updated' => 0,
  1814.   );
  1815.   $existing_item = FALSE;
  1816.   if (isset($item['mlid'])) {
  1817.     $existing_item = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $item['mlid']));
  1818.   }
  1819.  
  1820.   if (isset($item['plid'])) {
  1821.     $parent = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $item['plid']));
  1822.   }
  1823.   else {
  1824.     // Find the parent - it must be unique.
  1825.     $parent_path = $item['link_path'];
  1826.     $where = "WHERE link_path = '%s'";
  1827.     // Only links derived from router items should have module == 'system', and
  1828.     // we want to find the parent even if it's in a different menu.
  1829.     if ($item['module'] == 'system') {
  1830.       $where .= " AND module = '%s'";
  1831.       $arg2 = 'system';
  1832.     }
  1833.     else {
  1834.       // If not derived from a router item, we respect the specified menu name.
  1835.       $where .= " AND menu_name = '%s'";
  1836.       $arg2 = $item['menu_name'];
  1837.     }
  1838.     do {
  1839.       $parent = FALSE;
  1840.       $parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
  1841.       $result = db_query("SELECT COUNT(*) FROM {menu_links} ". $where, $parent_path, $arg2);
  1842.       // Only valid if we get a unique result.
  1843.       if (db_result($result) == 1) {
  1844.         $parent = db_fetch_array(db_query("SELECT * FROM {menu_links} ". $where, $parent_path, $arg2));
  1845.       }
  1846.     } while ($parent === FALSE && $parent_path);
  1847.   }
  1848.   if ($parent !== FALSE) {
  1849.     $item['menu_name'] = $parent['menu_name'];
  1850.   }
  1851.   $menu_name = $item['menu_name'];
  1852.   // Menu callbacks need to be in the links table for breadcrumbs, but can't
  1853.   // be parents if they are generated directly from a router item.
  1854.   if (empty($parent['mlid']) || $parent['hidden'] < 0) {
  1855.     $item['plid'] =  0;
  1856.   }
  1857.   else {
  1858.     $item['plid'] = $parent['mlid'];
  1859.   }
  1860.  
  1861.   if (!$existing_item) {
  1862.     db_query("INSERT INTO {menu_links} (
  1863.        menu_name, plid, link_path,
  1864.       hidden, external, has_children,
  1865.       expanded, weight,
  1866.       module, link_title, options,
  1867.       customized, updated) VALUES (
  1868.       '%s', %d, '%s',
  1869.       %d, %d, %d,
  1870.       %d, %d,
  1871.       '%s', '%s', '%s', %d, %d)",
  1872.       $item['menu_name'], $item['plid'], $item['link_path'],
  1873.       $item['hidden'], $item['_external'], $item['has_children'],
  1874.       $item['expanded'], $item['weight'],
  1875.       $item['module'],  $item['link_title'], serialize($item['options']),
  1876.       $item['customized'], $item['updated']);
  1877.     $item['mlid'] = db_last_insert_id('menu_links', 'mlid');
  1878.   }
  1879.  
  1880.   if (!$item['plid']) {
  1881.     $item['p1'] = $item['mlid'];
  1882.     for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
  1883.       $item["p$i"] = 0;
  1884.     }
  1885.     $item['depth'] = 1;
  1886.   }
  1887.   else {
  1888.     // Cannot add beyond the maximum depth.
  1889.     if ($item['has_children'] && $existing_item) {
  1890.       $limit = MENU_MAX_DEPTH - menu_link_children_relative_depth($existing_item) - 1;
  1891.     }
  1892.     else {
  1893.       $limit = MENU_MAX_DEPTH - 1;
  1894.     }
  1895.     if ($parent['depth'] > $limit) {
  1896.       return FALSE;
  1897.     }
  1898.     $item['depth'] = $parent['depth'] + 1;
  1899.     _menu_link_parents_set($item, $parent);
  1900.   }
  1901.   // Need to check both plid and menu_name, since plid can be 0 in any menu.
  1902.   if ($existing_item && ($item['plid'] != $existing_item['plid'] || $menu_name != $existing_item['menu_name'])) {
  1903.     _menu_link_move_children($item, $existing_item);
  1904.   }
  1905.   // Find the callback. During the menu update we store empty paths to be
  1906.   // fixed later, so we skip this.
  1907.   if (!isset($_SESSION['system_update_6021']) && (empty($item['router_path'])  || !$existing_item || ($existing_item['link_path'] != $item['link_path']))) {
  1908.     if ($item['_external']) {
  1909.       $item['router_path'] = '';
  1910.     }
  1911.     else {
  1912.       // Find the router path which will serve this path.
  1913.       $item['parts'] = explode('/', $item['link_path'], MENU_MAX_PARTS);
  1914.       $item['router_path'] = _menu_find_router_path($menu, $item['link_path']);
  1915.     }
  1916.   }
  1917.   db_query("UPDATE {menu_links} SET menu_name = '%s', plid = %d, link_path = '%s',
  1918.     router_path = '%s', hidden = %d, external = %d, has_children = %d,
  1919.     expanded = %d, weight = %d, depth = %d,
  1920.     p1 = %d, p2 = %d, p3 = %d, p4 = %d, p5 = %d, p6 = %d, p7 = %d, p8 = %d, p9 = %d,
  1921.     module = '%s', link_title = '%s', options = '%s', customized = %d WHERE mlid = %d",
  1922.     $item['menu_name'], $item['plid'], $item['link_path'],
  1923.     $item['router_path'], $item['hidden'], $item['_external'], $item['has_children'],
  1924.     $item['expanded'], $item['weight'],  $item['depth'],
  1925.     $item['p1'], $item['p2'], $item['p3'], $item['p4'], $item['p5'], $item['p6'], $item['p7'], $item['p8'], $item['p9'],
  1926.     $item['module'],  $item['link_title'], serialize($item['options']), $item['customized'], $item['mlid']);
  1927.   // Check the has_children status of the parent.
  1928.   _menu_update_parental_status($item);
  1929.   menu_cache_clear($menu_name);
  1930.   if ($existing_item && $menu_name != $existing_item['menu_name']) {
  1931.     menu_cache_clear($existing_item['menu_name']);
  1932.   }
  1933.  
  1934.   _menu_clear_page_cache();
  1935.   return $item['mlid'];
  1936. }
  1937.  
  1938. /**
  1939.  * Helper function to clear the page and block caches at most twice per page load.
  1940.  */
  1941. function _menu_clear_page_cache() {
  1942.   static $cache_cleared = 0;
  1943.  
  1944.   // Clear the page and block caches, but at most twice, including at
  1945.   //  the end of the page load when there are multple links saved or deleted.
  1946.   if (empty($cache_cleared)) {
  1947.     cache_clear_all();
  1948.     // Keep track of which menus have expanded items.
  1949.     _menu_set_expanded_menus();
  1950.     $cache_cleared = 1;
  1951.   }
  1952.   elseif ($cache_cleared == 1) {
  1953.     register_shutdown_function('cache_clear_all');
  1954.     // Keep track of which menus have expanded items.
  1955.     register_shutdown_function('_menu_set_expanded_menus');
  1956.     $cache_cleared = 2;
  1957.   }
  1958. }
  1959.  
  1960. /**
  1961.  * Helper function to update a list of menus with expanded items
  1962.  */
  1963. function _menu_set_expanded_menus() {
  1964.   $names = array();
  1965.   $result = db_query("SELECT menu_name FROM {menu_links} WHERE expanded != 0 GROUP BY menu_name");
  1966.   while ($n = db_fetch_array($result)) {
  1967.     $names[] = $n['menu_name'];
  1968.   }
  1969.   variable_set('menu_expanded', $names);
  1970. }
  1971.  
  1972. /**
  1973.  * Find the router path which will serve this path.
  1974.  *
  1975.  * @param $menu
  1976.  *  The full built menu.
  1977.  * @param $link_path
  1978.  *  The path for we are looking up its router path.
  1979.  * @return
  1980.  *  A path from $menu keys or empty if $link_path points to a nonexisting
  1981.  *  place.
  1982.  */
  1983. function _menu_find_router_path($menu, $link_path) {
  1984.   $parts = explode('/', $link_path, MENU_MAX_PARTS);
  1985.   $router_path = $link_path;
  1986.   if (!isset($menu[$router_path])) {
  1987.     list($ancestors) = menu_get_ancestors($parts);
  1988.     $ancestors[] = '';
  1989.     foreach ($ancestors as $key => $router_path) {
  1990.       if (isset($menu[$router_path])) {
  1991.         break;
  1992.       }
  1993.     }
  1994.   }
  1995.   return $router_path;
  1996. }
  1997.  
  1998. /**
  1999.  * Insert, update or delete an uncustomized menu link related to a module.
  2000.  *
  2001.  * @param $module
  2002.  *   The name of the module.
  2003.  * @param $op
  2004.  *   Operation to perform: insert, update or delete.
  2005.  * @param $link_path
  2006.  *   The path this link points to.
  2007.  * @param $link_title
  2008.  *   Title of the link to insert or new title to update the link to.
  2009.  *   Unused for delete.
  2010.  * @return
  2011.  *   The insert op returns the mlid of the new item. Others op return NULL.
  2012.  */
  2013. function menu_link_maintain($module, $op, $link_path, $link_title) {
  2014.   switch ($op) {
  2015.     case 'insert':
  2016.       $menu_link = array(
  2017.         'link_title' => $link_title,
  2018.         'link_path' => $link_path,
  2019.         'module' => $module,
  2020.       );
  2021.       return menu_link_save($menu_link);
  2022.       break;
  2023.     case 'update':
  2024.       db_query("UPDATE {menu_links} SET link_title = '%s' WHERE link_path = '%s' AND customized = 0 AND module = '%s'", $link_title, $link_path, $module);
  2025.       menu_cache_clear();
  2026.       break;
  2027.     case 'delete':
  2028.       menu_link_delete(NULL, $link_path);
  2029.       break;
  2030.   }
  2031. }
  2032.  
  2033. /**
  2034.  * Find the depth of an item's children relative to its depth.
  2035.  *
  2036.  * For example, if the item has a depth of 2, and the maximum of any child in
  2037.  * the menu link tree is 5, the relative depth is 3.
  2038.  *
  2039.  * @param $item
  2040.  *   An array representing a menu link item.
  2041.  * @return
  2042.  *   The relative depth, or zero.
  2043.  *
  2044.  */
  2045. function menu_link_children_relative_depth($item) {
  2046.   $i = 1;
  2047.   $match = '';
  2048.   $args[] = $item['menu_name'];
  2049.   $p = 'p1';
  2050.   while ($i <= MENU_MAX_DEPTH && $item[$p]) {
  2051.     $match .= " AND $p = %d";
  2052.     $args[] = $item[$p];
  2053.     $p = 'p'. ++$i;
  2054.   }
  2055.  
  2056.   $max_depth = db_result(db_query_range("SELECT depth FROM {menu_links} WHERE menu_name = '%s'". $match ." ORDER BY depth DESC", $args, 0, 1));
  2057.  
  2058.   return ($max_depth > $item['depth']) ? $max_depth - $item['depth'] : 0;
  2059. }
  2060.  
  2061. /**
  2062.  * Update the children of a menu link that's being moved.
  2063.  *
  2064.  * The menu name, parents (p1 - p6), and depth are updated for all children of
  2065.  * the link, and the has_children status of the previous parent is updated.
  2066.  */
  2067. function _menu_link_move_children($item, $existing_item) {
  2068.  
  2069.   $args[] = $item['menu_name'];
  2070.   $set[] = "menu_name = '%s'";
  2071.  
  2072.   $i = 1;
  2073.   while ($i <= $item['depth']) {
  2074.     $p = 'p'. $i++;
  2075.     $set[] = "$p = %d";
  2076.     $args[] = $item[$p];
  2077.   }
  2078.   $j = $existing_item['depth'] + 1;
  2079.   while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) {
  2080.     $set[] = 'p'. $i++ .' = p'. $j++;
  2081.   }
  2082.   while ($i <= MENU_MAX_DEPTH) {
  2083.     $set[] = 'p'. $i++ .' = 0';
  2084.   }
  2085.  
  2086.   $shift = $item['depth'] - $existing_item['depth'];
  2087.   if ($shift < 0) {
  2088.     $args[] = -$shift;
  2089.     $set[] = 'depth = depth - %d';
  2090.   }
  2091.   elseif ($shift > 0) {
  2092.     // The order of $set must be reversed so the new values don't overwrite the
  2093.     // old ones before they can be used because "Single-table UPDATE
  2094.     // assignments are generally evaluated from left to right"
  2095.     // see: http://dev.mysql.com/doc/refman/5.0/en/update.html
  2096.     $set = array_reverse($set);
  2097.     $args = array_reverse($args);
  2098.  
  2099.     $args[] = $shift;
  2100.     $set[] = 'depth = depth + %d';
  2101.   }
  2102.   $where[] = "menu_name = '%s'";
  2103.   $args[] = $existing_item['menu_name'];
  2104.   $p = 'p1';
  2105.   for ($i = 1; $i <= MENU_MAX_DEPTH && $existing_item[$p]; $p = 'p'. ++$i) {
  2106.     $where[] = "$p = %d";
  2107.     $args[] = $existing_item[$p];
  2108.   }
  2109.  
  2110.   db_query("UPDATE {menu_links} SET ". implode(', ', $set) ." WHERE ". implode(' AND ', $where), $args);
  2111.   // Check the has_children status of the parent, while excluding this item.
  2112.   _menu_update_parental_status($existing_item, TRUE);
  2113. }
  2114.  
  2115. /**
  2116.  * Check and update the has_children status for the parent of a link.
  2117.  */
  2118. function _menu_update_parental_status($item, $exclude = FALSE) {
  2119.   // If plid == 0, there is nothing to update.
  2120.   if ($item['plid']) {
  2121.     // We may want to exclude the passed link as a possible child.
  2122.     $where = $exclude ? " AND mlid != %d" : '';
  2123.     // Check if at least one visible child exists in the table.
  2124.     $parent_has_children = (bool)db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE menu_name = '%s' AND plid = %d AND hidden = 0". $where, $item['menu_name'], $item['plid'], $item['mlid'], 0, 1));
  2125.     db_query("UPDATE {menu_links} SET has_children = %d WHERE mlid = %d", $parent_has_children, $item['plid']);
  2126.   }
  2127. }
  2128.  
  2129. /**
  2130.  * Helper function that sets the p1..p9 values for a menu link being saved.
  2131.  */
  2132. function _menu_link_parents_set(&$item, $parent) {
  2133.   $i = 1;
  2134.   while ($i < $item['depth']) {
  2135.     $p = 'p'. $i++;
  2136.     $item[$p] = $parent[$p];
  2137.   }
  2138.   $p = 'p'. $i++;
  2139.   // The parent (p1 - p9) corresponding to the depth always equals the mlid.
  2140.   $item[$p] = $item['mlid'];
  2141.   while ($i <= MENU_MAX_DEPTH) {
  2142.     $p = 'p'. $i++;
  2143.     $item[$p] = 0;
  2144.   }
  2145. }
  2146.  
  2147. /**
  2148.  * Helper function to build the router table based on the data from hook_menu.
  2149.  */
  2150. function _menu_router_build($callbacks) {
  2151.   // First pass: separate callbacks from paths, making paths ready for
  2152.   // matching. Calculate fitness, and fill some default values.
  2153.   $menu = array();
  2154.   foreach ($callbacks as $path => $item) {
  2155.     $load_functions = array();
  2156.     $to_arg_functions = array();
  2157.     $fit = 0;
  2158.     $move = FALSE;
  2159.  
  2160.     $parts = explode('/', $path, MENU_MAX_PARTS);
  2161.     $number_parts = count($parts);
  2162.     // We store the highest index of parts here to save some work in the fit
  2163.     // calculation loop.
  2164.     $slashes = $number_parts - 1;
  2165.     // Extract load and to_arg functions.
  2166.     foreach ($parts as $k => $part) {
  2167.       $match = FALSE;
  2168.       if (preg_match('/^%([a-z_]*)$/', $part, $matches)) {
  2169.         if (empty($matches[1])) {
  2170.           $match = TRUE;
  2171.           $load_functions[$k] = NULL;
  2172.         }
  2173.         else {
  2174.           if (function_exists($matches[1] .'_to_arg')) {
  2175.             $to_arg_functions[$k] = $matches[1] .'_to_arg';
  2176.             $load_functions[$k] = NULL;
  2177.             $match = TRUE;
  2178.           }
  2179.           if (function_exists($matches[1] .'_load')) {
  2180.             $function = $matches[1] .'_load';
  2181.             // Create an array of arguments that will be passed to the _load
  2182.             // function when this menu path is checked, if 'load arguments'
  2183.             // exists.
  2184.             $load_functions[$k] = isset($item['load arguments']) ? array($function => $item['load arguments']) : $function;
  2185.             $match = TRUE;
  2186.           }
  2187.         }
  2188.       }
  2189.       if ($match) {
  2190.         $parts[$k] = '%';
  2191.       }
  2192.       else {
  2193.         $fit |=  1 << ($slashes - $k);
  2194.       }
  2195.     }
  2196.     if ($fit) {
  2197.       $move = TRUE;
  2198.     }
  2199.     else {
  2200.       // If there is no %, it fits maximally.
  2201.       $fit = (1 << $number_parts) - 1;
  2202.     }
  2203.     $masks[$fit] = 1;
  2204.     $item['load_functions'] = empty($load_functions) ? '' : serialize($load_functions);
  2205.     $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions);
  2206.     $item += array(
  2207.       'title' => '',
  2208.       'weight' => 0,
  2209.       'type' => MENU_NORMAL_ITEM,
  2210.       '_number_parts' => $number_parts,
  2211.       '_parts' => $parts,
  2212.       '_fit' => $fit,
  2213.     );
  2214.     $item += array(
  2215.       '_visible' => (bool)($item['type'] & MENU_VISIBLE_IN_BREADCRUMB),
  2216.       '_tab' => (bool)($item['type'] & MENU_IS_LOCAL_TASK),
  2217.     );
  2218.     if ($move) {
  2219.       $new_path = implode('/', $item['_parts']);
  2220.       $menu[$new_path] = $item;
  2221.       $sort[$new_path] = $number_parts;
  2222.     }
  2223.     else {
  2224.       $menu[$path] = $item;
  2225.       $sort[$path] = $number_parts;
  2226.     }
  2227.   }
  2228.   array_multisort($sort, SORT_NUMERIC, $menu);
  2229.  
  2230.   // Apply inheritance rules.
  2231.   foreach ($menu as $path => $v) {
  2232.     $item = &$menu[$path];
  2233.     if (!$item['_tab']) {
  2234.       // Non-tab items.
  2235.       $item['tab_parent'] = '';
  2236.       $item['tab_root'] = $path;
  2237.     }
  2238.     for ($i = $item['_number_parts'] - 1; $i; $i--) {
  2239.       $parent_path = implode('/', array_slice($item['_parts'], 0, $i));
  2240.       if (isset($menu[$parent_path])) {
  2241.  
  2242.         $parent = $menu[$parent_path];
  2243.  
  2244.         if (!isset($item['tab_parent'])) {
  2245.           // Parent stores the parent of the path.
  2246.           $item['tab_parent'] = $parent_path;
  2247.         }
  2248.         if (!isset($item['tab_root']) && !$parent['_tab']) {
  2249.           $item['tab_root'] = $parent_path;
  2250.         }
  2251.         // If a callback is not found, we try to find the first parent that
  2252.         // has a callback.
  2253.         if (!isset($item['access callback']) && isset($parent['access callback'])) {
  2254.           $item['access callback'] = $parent['access callback'];
  2255.           if (!isset($item['access arguments']) && isset($parent['access arguments'])) {
  2256.             $item['access arguments'] = $parent['access arguments'];
  2257.           }
  2258.         }
  2259.         // Same for page callbacks.
  2260.         if (!isset($item['page callback']) && isset($parent['page callback'])) {
  2261.           $item['page callback'] = $parent['page callback'];
  2262.           if (!isset($item['page arguments']) && isset($parent['page arguments'])) {
  2263.             $item['page arguments'] = $parent['page arguments'];
  2264.           }
  2265.           if (!isset($item['file']) && isset($parent['file'])) {
  2266.             $item['file'] = $parent['file'];
  2267.           }
  2268.           if (!isset($item['file path']) && isset($parent['file path'])) {
  2269.             $item['file path'] = $parent['file path'];
  2270.           }
  2271.         }
  2272.       }
  2273.     }
  2274.     if (!isset($item['access callback']) && isset($item['access arguments'])) {
  2275.       // Default callback.
  2276.       $item['access callback'] = 'user_access';
  2277.     }
  2278.     if (!isset($item['access callback']) || empty($item['page callback'])) {
  2279.       $item['access callback'] = 0;
  2280.     }
  2281.     if (is_bool($item['access callback'])) {
  2282.       $item['access callback'] = intval($item['access callback']);
  2283.     }
  2284.  
  2285.     $item += array(
  2286.       'access arguments' => array(),
  2287.       'access callback' => '',
  2288.       'page arguments' => array(),
  2289.       'page callback' => '',
  2290.       'block callback' => '',
  2291.       'title arguments' => array(),
  2292.       'title callback' => 't',
  2293.       'description' => '',
  2294.       'position' => '',
  2295.       'tab_parent' => '',
  2296.       'tab_root' => $path,
  2297.       'path' => $path,
  2298.       'file' => '',
  2299.       'file path' => '',
  2300.       'include file' => '',
  2301.     );
  2302.  
  2303.     // Calculate out the file to be included for each callback, if any.
  2304.     if ($item['file']) {
  2305.       $file_path = $item['file path'] ? $item['file path'] : drupal_get_path('module', $item['module']);
  2306.       $item['include file'] = $file_path .'/'. $item['file'];
  2307.     }
  2308.  
  2309.     $title_arguments = $item['title arguments'] ? serialize($item['title arguments']) : '';
  2310.     db_query("INSERT INTO {menu_router}
  2311.       (path, load_functions, to_arg_functions, access_callback,
  2312.       access_arguments, page_callback, page_arguments, fit,
  2313.       number_parts, tab_parent, tab_root,
  2314.       title, title_callback, title_arguments,
  2315.       type, block_callback, description, position, weight, file)
  2316.       VALUES ('%s', '%s', '%s', '%s',
  2317.       '%s', '%s', '%s', %d,
  2318.       %d, '%s', '%s',
  2319.       '%s', '%s', '%s',
  2320.       %d, '%s', '%s', '%s', %d, '%s')",
  2321.       $path, $item['load_functions'], $item['to_arg_functions'], $item['access callback'],
  2322.       serialize($item['access arguments']), $item['page callback'], serialize($item['page arguments']), $item['_fit'],
  2323.       $item['_number_parts'], $item['tab_parent'], $item['tab_root'],
  2324.       $item['title'], $item['title callback'], $title_arguments,
  2325.       $item['type'], $item['block callback'], $item['description'], $item['position'], $item['weight'], $item['include file']);
  2326.   }
  2327.   // Sort the masks so they are in order of descending fit, and store them.
  2328.   $masks = array_keys($masks);
  2329.   rsort($masks);
  2330.   variable_set('menu_masks', $masks);
  2331.   return $menu;
  2332. }
  2333.  
  2334. /**
  2335.  * Returns TRUE if a path is external (e.g. http://example.com).
  2336.  */
  2337. function menu_path_is_external($path) {
  2338.   $colonpos = strpos($path, ':');
  2339.   return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && filter_xss_bad_protocol($path, FALSE) == check_plain($path);
  2340. }
  2341.  
  2342. /**
  2343.  * Checks whether the site is off-line for maintenance.
  2344.  *
  2345.  * This function will log the current user out and redirect to front page
  2346.  * if the current user has no 'administer site configuration' permission.
  2347.  *
  2348.  * @return
  2349.  *   FALSE if the site is not off-line or its the login page or the user has
  2350.  *     'administer site configuration' permission.
  2351.  *   TRUE for anonymous users not on the login page if the site is off-line.
  2352.  */
  2353. function _menu_site_is_offline() {
  2354.   // Check if site is set to off-line mode.
  2355.   if (variable_get('site_offline', 0)) {
  2356.     // Check if the user has administration privileges.
  2357.     if (user_access('administer site configuration')) {
  2358.       // Ensure that the off-line message is displayed only once [allowing for
  2359.       // page redirects], and specifically suppress its display on the site
  2360.       // maintenance page.
  2361.       if (drupal_get_normal_path($_GET['q']) != 'admin/settings/site-maintenance') {
  2362.         drupal_set_message(t('Operating in off-line mode.'), 'status', FALSE);
  2363.       }
  2364.     }
  2365.     else {
  2366.       // Anonymous users get a FALSE at the login prompt, TRUE otherwise.
  2367.       if (user_is_anonymous()) {
  2368.         return $_GET['q'] != 'user' && $_GET['q'] != 'user/login';
  2369.       }
  2370.       // Logged in users are unprivileged here, so they are logged out.
  2371.       require_once drupal_get_path('module', 'user') .'/user.pages.inc';
  2372.       user_logout();
  2373.     }
  2374.   }
  2375.   return FALSE;
  2376. }
  2377.  
  2378. /**
  2379.  * Validates the path of a menu link being created or edited.
  2380.  *
  2381.  * @return
  2382.  *   TRUE if it is a valid path AND the current user has access permission,
  2383.  *   FALSE otherwise.
  2384.  */
  2385. function menu_valid_path($form_item) {
  2386.   global $menu_admin;
  2387.   $item = array();
  2388.   $path = $form_item['link_path'];
  2389.   // We indicate that a menu administrator is running the menu access check.
  2390.   $menu_admin = TRUE;
  2391.   if ($path == '<front>' || menu_path_is_external($path)) {
  2392.     $item = array('access' => TRUE);
  2393.   }
  2394.   elseif (preg_match('/\/\%/', $path)) {
  2395.     // Path is dynamic (ie 'user/%'), so check directly against menu_router table.
  2396.     if ($item = db_fetch_array(db_query("SELECT * FROM {menu_router} where path = '%s' ", $path))) {
  2397.       $item['link_path']  = $form_item['link_path'];
  2398.       $item['link_title'] = $form_item['link_title'];
  2399.       $item['external']   = FALSE;
  2400.       $item['options'] = '';
  2401.       _menu_link_translate($item);
  2402.     }
  2403.   }
  2404.   else {
  2405.     $item = menu_get_item($path);
  2406.   }
  2407.   $menu_admin = FALSE;
  2408.   return $item && $item['access'];
  2409. }
  2410.  
  2411. /**
  2412.  * @} End of "defgroup menu".
  2413.  */
  2414.