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