Spike PHPCoverage Details: menu.inc

Line #FrequencySource Line
1 <?php
2 // $Id: menu.inc,v 1.267 2008/03/21 08:32:24 dries 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) {
2021  $number_parts = count($parts);
203   $placeholders = array();
204   $ancestors = array();
2051  $length =  $number_parts - 1;
2061  $end = (1 << $number_parts) - 1;
2071  $masks = variable_get('menu_masks', array());
208   // Only examine patterns that actually exist as router items (the masks).
2091  foreach ($masks as $i) {
2101    if ($i > $end) {
211       // Only look at masks that are not longer than the path of interest.
2121      continue;
213     }
2141    elseif ($i < (1 << $length)) {
215       // We have exhausted the masks of a given length, so decrease the length.
2161      --$length;
217     }
2181    $current = '';
2191    for ($j = $length; $j >= 0; $j--) {
2201      if ($i & (1 << $j)) {
2211        $current .= $parts[$length - $j];
222       }
223       else {
2241        $current .= '%';
225       }
2261      if ($j) {
2271        $current .= '/';
228       }
229     }
2301    $placeholders[] = "'%s'";
2311    $ancestors[] = $current;
232   }
2331  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   if (variable_get('menu_rebuild_needed', FALSE)) {
339     menu_rebuild();
340   }
341   if ($router_item = menu_get_item($path)) {
342     if ($router_item['access']) {
343       if ($router_item['file']) {
344         require_once($router_item['file']);
345       }
346       return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
347     }
348     else {
349       return MENU_ACCESS_DENIED;
350     }
351   }
352   return MENU_NOT_FOUND;
353 }
354 
355 /**
356  * Loads objects into the map as defined in the $item['load_functions'].
357  *
358  * @param $item
359  *   A menu router or menu link item
360  * @param $map
361  *   An array of path arguments (ex: array('node', '5'))
362  * @return
363  *   Returns TRUE for success, FALSE if an object cannot be loaded.
364  *   Names of object loading functions are placed in $item['load_functions'].
365  *   Loaded objects are placed in $map[]; keys are the same as keys in the
366  *   $item['load_functions'] array.
367  *   $item['access'] is set to FALSE if an object cannot be loaded.
368  */
369 function _menu_load_objects(&$item, &$map) {
370   if ($load_functions = $item['load_functions']) {
371     // If someone calls this function twice, then unserialize will fail.
372     if ($load_functions_unserialized = unserialize($load_functions)) {
373       $load_functions = $load_functions_unserialized;
374     }
375     $path_map = $map;
376     foreach ($load_functions as $index => $function) {
377       if ($function) {
378         $value = isset($path_map[$index]) ? $path_map[$index] : '';
379         if (is_array($function)) {
380           // Set up arguments for the load function. These were pulled from
381           // 'load arguments' in the hook_menu() entry, but they need
382           // some processing. In this case the $function is the key to the
383           // load_function array, and the value is the list of arguments.
384           list($function, $args) = each($function);
385           $load_functions[$index] = $function;
386 
387           // Some arguments are placeholders for dynamic items to process.
388           foreach ($args as $i => $arg) {
389             if ($arg === '%index') {
390               // Pass on argument index to the load function, so multiple
391               // occurances of the same placeholder can be identified.
392               $args[$i] = $index;
393             }
394             if ($arg === '%map') {
395               // Pass on menu map by reference. The accepting function must
396               // also declare this as a reference if it wants to modify
397               // the map.
398               $args[$i] = &$map;
399             }
400             if (is_int($arg)) {
401               $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : '';
402             }
403           }
404           array_unshift($args, $value);
405           $return = call_user_func_array($function, $args);
406         }
407         else {
408           $return = $function($value);
409         }
410         // If callback returned an error or there is no callback, trigger 404.
411         if ($return === FALSE) {
412           $item['access'] = FALSE;
413           $map = FALSE;
414           return FALSE;
415         }
416         $map[$index] = $return;
417       }
418     }
419     $item['load_functions'] = $load_functions;
420   }
421   return TRUE;
422 }
423 
424 /**
425  * Check access to a menu item using the access callback
426  *
427  * @param $item
428  *   A menu router or menu link item
429  * @param $map
430  *   An array of path arguments (ex: array('node', '5'))
431  * @return
432  *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
433  */
434 function _menu_check_access(&$item, $map) {
435   // Determine access callback, which will decide whether or not the current
436   // user has access to this path.
437   $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']);
438   // Check for a TRUE or FALSE value.
439   if (is_numeric($callback)) {
440     $item['access'] = (bool)$callback;
441   }
442   else {
443     $arguments = menu_unserialize($item['access_arguments'], $map);
444     // As call_user_func_array is quite slow and user_access is a very common
445     // callback, it is worth making a special case for it.
446     if ($callback == 'user_access') {
447       $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
448     }
449     else {
450       $item['access'] = call_user_func_array($callback, $arguments);
451     }
452   }
453 }
454 
455 /**
456  * Localize the router item title using t() or another callback.
457  *
458  * Translate the title and description to allow storage of English title
459  * strings in the database, yet display of them in the language required
460  * by the current user.
461  *
462  * @param $item
463  *   A menu router item or a menu link item.
464  * @param $map
465  *   The path as an array with objects already replaced. E.g., for path
466  *   node/123 $map would be array('node', $node) where $node is the node
467  *   object for node 123.
468  * @param $link_translate
469  *   TRUE if we are translating a menu link item; FALSE if we are
470  *   translating a menu router item.
471  * @return
472  *   No return value.
473  *   $item['title'] is localized according to $item['title_callback'].
474  *   If an item's callback is check_plain(), $item['options']['html'] becomes
475  *   TRUE.
476  *   $item['description'] is translated using t().
477  *   When doing link translation and the $item['options']['attributes']['title']
478  *   (link title attribute) matches the description, it is translated as well.
479  */
480 function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
481   $callback = $item['title_callback'];
482   $item['localized_options'] = $item['options'];
483   // If we are not doing link translation or if the title matches the
484   // link title of its router item, localize it.
485   if (!$link_translate || (!empty($item['title']) && ($item['title'] == $item['link_title']))) {
486     // t() is a special case. Since it is used very close to all the time,
487     // we handle it directly instead of using indirect, slower methods.
488     if ($callback == 't') {
489       if (empty($item['title_arguments'])) {
490         $item['title'] = t($item['title']);
491       }
492       else {
493         $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
494       }
495     }
496     elseif ($callback) {
497       if (empty($item['title_arguments'])) {
498         $item['title'] = $callback($item['title']);
499       }
500       else {
501         $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
502       }
503       // Avoid calling check_plain again on l() function.
504       if ($callback == 'check_plain') {
505         $item['localized_options']['html'] = TRUE;
506       }
507     }
508   }
509   elseif ($link_translate) {
510     $item['title'] = $item['link_title'];
511   }
512 
513   // Translate description, see the motivation above.
514   if (!empty($item['description'])) {
515     $original_description = $item['description'];
516     $item['description'] = t($item['description']);
517     if ($link_translate && $item['options']['attributes']['title'] == $original_description) {
518       $item['localized_options']['attributes']['title'] = $item['description'];
519     }
520   }
521 }
522 
523 /**
524  * Handles dynamic path translation and menu access control.
525  *
526  * When a user arrives on a page such as node/5, this function determines
527  * what "5" corresponds to, by inspecting the page's menu path definition,
528  * node/%node. This will call node_load(5) to load the corresponding node
529  * object.
530  *
531  * It also works in reverse, to allow the display of tabs and menu items which
532  * contain these dynamic arguments, translating node/%node to node/5.
533  *
534  * Translation of menu item titles and descriptions are done here to
535  * allow for storage of English strings in the database, and translation
536  * to the language required to generate the current page
537  *
538  * @param $router_item
539  *   A menu router item
540  * @param $map
541  *   An array of path arguments (ex: array('node', '5'))
542  * @param $to_arg
543  *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
544  *   path from the menu table, for example tabs.
545  * @return
546  *   Returns the map with objects loaded as defined in the
547  *   $item['load_functions. $item['access'] becomes TRUE if the item is
548  *   accessible, FALSE otherwise. $item['href'] is set according to the map.
549  *   If an error occurs during calling the load_functions (like trying to load
550  *   a non existing node) then this function return FALSE.
551  */
552 function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
553   $path_map = $map;
554   if (!_menu_load_objects($router_item, $map)) {
555     // An error occurred loading an object.
556     $router_item['access'] = FALSE;
557     return FALSE;
558   }
559   if ($to_arg) {
560     _menu_link_map_translate($path_map, $router_item['to_arg_functions']);
561   }
562 
563   // Generate the link path for the page request or local tasks.
564   $link_map = explode('/', $router_item['path']);
565   for ($i = 0; $i < $router_item['number_parts']; $i++) {
566     if ($link_map[$i] == '%') {
567       $link_map[$i] = $path_map[$i];
568     }
569   }
570   $router_item['href'] = implode('/', $link_map);
571   $router_item['options'] = array();
572   _menu_check_access($router_item, $map);
573 
574   _menu_item_localize($router_item, $map);
575 
576   return $map;
577 }
578 
579 /**
580  * This function translates the path elements in the map using any to_arg
581  * helper function. These functions take an argument and return an object.
582  * See http://drupal.org/node/109153 for more information.
583  *
584  * @param map
585  *   An array of path arguments (ex: array('node', '5'))
586  * @param $to_arg_functions
587  *   An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
588  */
589 function _menu_link_map_translate(&$map, $to_arg_functions) {
590   if ($to_arg_functions) {
591     $to_arg_functions = unserialize($to_arg_functions);
592     foreach ($to_arg_functions as $index => $function) {
593       // Translate place-holders into real values.
594       $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
595       if (!empty($map[$index]) || isset($arg)) {
596         $map[$index] = $arg;
597       }
598       else {
599         unset($map[$index]);
600       }
601     }
602   }
603 }
604 
605 function menu_tail_to_arg($arg, $map, $index) {
606   return implode('/', array_slice($map, $index));
607 }
608 
609 /**
610  * This function is similar to _menu_translate() but does link-specific
611  * preparation such as always calling to_arg functions
612  *
613  * @param $item
614  *   A menu link
615  * @return
616  *   Returns the map of path arguments with objects loaded as defined in the
617  *   $item['load_functions'].
618  *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
619  *   $item['href'] is generated from link_path, possibly by to_arg functions.
620  *   $item['title'] is generated from link_title, and may be localized.
621  *   $item['options'] is unserialized; it is also changed within the call here
622  *   to $item['localized_options'] by _menu_item_localize().
623  */
624 function _menu_link_translate(&$item) {
625   $item['options'] = unserialize($item['options']);
626   if ($item['external']) {
627     $item['access'] = 1;
628     $map = array();
629     $item['href'] = $item['link_path'];
630     $item['title'] = $item['link_title'];
631     $item['localized_options'] = $item['options'];
632   }
633   else {
634     $map = explode('/', $item['link_path']);
635     _menu_link_map_translate($map, $item['to_arg_functions']);
636     $item['href'] = implode('/', $map);
637 
638     // Note - skip callbacks without real values for their arguments.
639     if (strpos($item['href'], '%') !== FALSE) {
640       $item['access'] = FALSE;
641       return FALSE;
642     }
643     // menu_tree_check_access() may set this ahead of time for links to nodes.
644     if (!isset($item['access'])) {
645       if (!_menu_load_objects($item, $map)) {
646         // An error occurred loading an object.
647         $item['access'] = FALSE;
648         return FALSE;
649       }
650       _menu_check_access($item, $map);
651     }
652 
653     _menu_item_localize($item, $map, TRUE);
654   }
655 
656   // Allow other customizations - e.g. adding a page-specific query string to the
657   // options array. For performance reasons we only invoke this hook if the link
658   // has the 'alter' flag set in the options array.
659   if (!empty($item['options']['alter'])) {
660     drupal_alter('translated_menu_link', $item, $map);
661   }
662 
663   return $map;
664 }
665 
666 /**
667  * Get a loaded object from a router item.
668  *
669  * menu_get_object() will provide you the current node on paths like node/5,
670  * node/5/revisions/48 etc. menu_get_object('user') will give you the user
671  * account on user/5 etc. Note - this function should never be called within a
672  * _to_arg function (like user_current_to_arg()) since this may result in an
673  * infinite recursion.
674  *
675  * @param $type
676  *   Type of the object. These appear in hook_menu definitons as %type. Core
677  *   provides aggregator_feed, aggregator_category, contact, filter_format,
678  *   forum_term, menu, menu_link, node, taxonomy_vocabulary, user. See the
679  *   relevant {$type}_load function for more on each. Defaults to node.
680  * @param $position
681  *   The expected position for $type object. For node/%node this is 1, for
682  *   comment/reply/%node this is 2. Defaults to 1.
683  * @param $path
684  *   See menu_get_item() for more on this. Defaults to the current path.
685  */
686 function menu_get_object($type = 'node', $position = 1, $path = NULL) {
687   $router_item = menu_get_item($path);
688   if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type .'_load') {
689     return $router_item['map'][$position];
690   }
691 }
692 
693 /**
694  * Render a menu tree based on the current path.
695  *
696  * The tree is expanded based on the current path and dynamic paths are also
697  * changed according to the defined to_arg functions (for example the 'My account'
698  * link is changed from user/% to a link with the current user's uid).
699  *
700  * @param $menu_name
701  *   The name of the menu.
702  * @return
703  *   The rendered HTML of that menu on the current page.
704  */
705 function menu_tree($menu_name = 'navigation') {
706   static $menu_output = array();
707 
708   if (!isset($menu_output[$menu_name])) {
709     $tree = menu_tree_page_data($menu_name);
710     $menu_output[$menu_name] = menu_tree_output($tree);
711   }
712   return $menu_output[$menu_name];
713 }
714 
715 /**
716  * Returns a rendered menu tree.
717  *
718  * @param $tree
719  *   A data structure representing the tree as returned from menu_tree_data.
720  * @return
721  *   The rendered HTML of that data structure.
722  */
723 function menu_tree_output($tree) {
724   $output = '';
725   $items = array();
726 
727   // Pull out just the menu items we are going to render so that we
728   // get an accurate count for the first/last classes.
729   foreach ($tree as $data) {
730     if (!$data['link']['hidden']) {
731       $items[] = $data;
732     }
733   }
734 
735   $num_items = count($items);
736   foreach ($items as $i => $data) {
737     $extra_class = NULL;
738     if ($i == 0) {
739       $extra_class = 'first';
740     }
741     if ($i == $num_items - 1) {
742       $extra_class = 'last';
743     }
744     $link = theme('menu_item_link', $data['link']);
745     if ($data['below']) {
746       $output .= theme('menu_item', $link, $data['link']['has_children'], menu_tree_output($data['below']), $data['link']['in_active_trail'], $extra_class);
747     }
748     else {
749       $output .= theme('menu_item', $link, $data['link']['has_children'], '', $data['link']['in_active_trail'], $extra_class);
750     }
751   }
752   return $output ? theme('menu_tree', $output) : '';
753 }
754 
755 /**
756  * Get the data structure representing a named menu tree.
757  *
758  * Since this can be the full tree including hidden items, the data returned
759  * may be used for generating an an admin interface or a select.
760  *
761  * @param $menu_name
762  *   The named menu links to return
763  * @param $item
764  *   A fully loaded menu link, or NULL.  If a link is supplied, only the
765  *   path to root will be included in the returned tree- as if this link
766  *   represented the current page in a visible menu.
767  * @return
768  *   An tree of menu links in an array, in the order they should be rendered.
769  */
770 function menu_tree_all_data($menu_name = 'navigation', $item = NULL) {
771   static $tree = array();
772 
773   // Use $mlid as a flag for whether the data being loaded is for the whole tree.
774   $mlid = isset($item['mlid']) ? $item['mlid'] : 0;
775   // Generate a cache ID (cid) specific for this $menu_name and $item.
776   $cid = 'links:'. $menu_name .':all-cid:'. $mlid;
777 
778   if (!isset($tree[$cid])) {
779     // If the static variable doesn't have the data, check {cache_menu}.
780     $cache = cache_get($cid, 'cache_menu');
781     if ($cache && isset($cache->data)) {
782       // If the cache entry exists, it will just be the cid for the actual data.
783       // This avoids duplication of large amounts of data.
784       $cache = cache_get($cache->data, 'cache_menu');
785       if ($cache && isset($cache->data)) {
786         $data = $cache->data;
787       }
788     }
789     // If the tree data was not in the cache, $data will be NULL.
790     if (!isset($data)) {
791       // Build and run the query, and build the tree.
792       if ($mlid) {
793         // The tree is for a single item, so we need to match the values in its
794         // p columns and 0 (the top level) with the plid values of other links.
795         $args = array(0);
796         for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
797           $args[] = $item["p$i"];
798         }
799         $args = array_unique($args);
800         $placeholders = implode(', ', array_fill(0, count($args), '%d'));
801         $where = ' AND ml.plid IN ('. $placeholders .')';
802         $parents = $args;
803         $parents[] = $item['mlid'];
804       }
805       else {
806         // Get all links in this menu.
807         $where = '';
808         $args = array();
809         $parents = array();
810       }
811       array_unshift($args, $menu_name);
812       // Select the links from the table, and recursively build the tree.  We
813       // LEFT JOIN since there is no match in {menu_router} for an external
814       // link.
815       $data['tree'] = menu_tree_data(db_query("
816         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.*
817         FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
818         WHERE ml.menu_name = '%s'". $where ."
819         ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC", $args), $parents);
820       $data['node_links'] = array();
821       menu_tree_collect_node_links($data['tree'], $data['node_links']);
822       // Cache the data, if it is not already in the cache.
823       $tree_cid = _menu_tree_cid($menu_name, $data);
824       if (!cache_get($tree_cid, 'cache_menu')) {
825         cache_set($tree_cid, $data, 'cache_menu');
826       }
827       // Cache the cid of the (shared) data using the menu and item-specific cid.
828       cache_set($cid, $tree_cid, 'cache_menu');
829     }
830     // Check access for the current user to each item in the tree.
831     menu_tree_check_access($data['tree'], $data['node_links']);
832     $tree[$cid] = $data['tree'];
833   }
834 
835   return $tree[$cid];
836 }
837 
838 /**
839  * Get the data structure representing a named menu tree, based on the current page.
840  *
841  * The tree order is maintained by storing each parent in an individual
842  * field, see http://drupal.org/node/141866 for more.
843  *
844  * @param $menu_name
845  *   The named menu links to return
846  * @return
847  *   An array of menu links, in the order they should be rendered. The array
848  *   is a list of associative arrays -- these have two keys, link and below.
849  *   link is a menu item, ready for theming as a link. Below represents the
850  *   submenu below the link if there is one, and it is a subtree that has the
851  *   same structure described for the top-level array.
852  */
853 function menu_tree_page_data($menu_name = 'navigation') {
854   static $tree = array();
855 
856   // Load the menu item corresponding to the current page.
857   if ($item = menu_get_item()) {
858     // Generate a cache ID (cid) specific for this page.
859     $cid = 'links:'. $menu_name .':page-cid:'. $item['href'] .':'. (int)$item['access'];
860 
861     if (!isset($tree[$cid])) {
862       // If the static variable doesn't have the data, check {cache_menu}.
863       $cache = cache_get($cid, 'cache_menu');
864       if ($cache && isset($cache->data)) {
865         // If the cache entry exists, it will just be the cid for the actual data.
866         // This avoids duplication of large amounts of data.
867         $cache = cache_get($cache->data, 'cache_menu');
868         if ($cache && isset($cache->data)) {
869           $data = $cache->data;
870         }
871       }
872       // If the tree data was not in the cache, $data will be NULL.
873       if (!isset($data)) {
874         // Build and run the query, and build the tree.
875         if ($item['access']) {
876           // Check whether a menu link exists that corresponds to the current path.
877           $args = array($menu_name, $item['href']);
878           $placeholders = "'%s'";
879           if (drupal_is_front_page()) {
880             $args[] = '<front>';
881             $placeholders .= ", '%s'";
882           }
883           $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));
884 
885           if (empty($parents)) {
886             // If no link exists, we may be on a local task that's not in the links.
887             // TODO: Handle the case like a local task on a specific node in the menu.
888             $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']));
889           }
890           // We always want all the top-level links with plid == 0.
891           $parents[] = '0';
892 
893           // Use array_values() so that the indices are numeric for array_merge().
894           $args = $parents = array_unique(array_values($parents));
895           $placeholders = implode(', ', array_fill(0, count($args), '%d'));
896           $expanded = variable_get('menu_expanded', array());
897           // Check whether the current menu has any links set to be expanded.
898           if (in_array($menu_name, $expanded)) {
899             // Collect all the links set to be expanded, and then add all of
900             // their children to the list as well.
901             do {
902               $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));
903               $num_rows = FALSE;
904               while ($item = db_fetch_array($result)) {
905                 $args[] = $item['mlid'];
906                 $num_rows = TRUE;
907               }
908               $placeholders = implode(', ', array_fill(0, count($args), '%d'));
909             } while ($num_rows);
910           }
911           array_unshift($args, $menu_name);
912         }
913         else {
914           // Show only the top-level menu items when access is denied.
915           $args = array($menu_name, '0');
916           $placeholders = '%d';
917           $parents = array();
918         }
919         // Select the links from the table, and recursively build the tree. We
920         // LEFT JOIN since there is no match in {menu_router} for an external
921         // link.
922         $data['tree'] = menu_tree_data(db_query("
923           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.*
924           FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
925           WHERE ml.menu_name = '%s' AND ml.plid IN (". $placeholders .")
926           ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC", $args), $parents);
927         $data['node_links'] = array();
928         menu_tree_collect_node_links($data['tree'], $data['node_links']);
929         // Cache the data, if it is not already in the cache.
930         $tree_cid = _menu_tree_cid($menu_name, $data);
931         if (!cache_get($tree_cid, 'cache_menu')) {
932           cache_set($tree_cid, $data, 'cache_menu');
933         }
934         // Cache the cid of the (shared) data using the page-specific cid.
935         cache_set($cid, $tree_cid, 'cache_menu');
936       }
937       // Check access for the current user to each item in the tree.
938       menu_tree_check_access($data['tree'], $data['node_links']);
939       $tree[$cid] = $data['tree'];
940     }
941     return $tree[$cid];
942   }
943 
944   return array();
945 }
946 
947 /**
948  * Helper function - compute the real cache ID for menu tree data.
949  */
950 function _menu_tree_cid($menu_name, $data) {
951   return 'links:'. $menu_name .':tree-data:'. md5(serialize($data));
952 }
953 
954 /**
955  * Recursive helper function - collect node links.
956  */
957 function menu_tree_collect_node_links(&$tree, &$node_links) {
958   foreach ($tree as $key => $v) {
959     if ($tree[$key]['link']['router_path'] == 'node/%') {
960       $nid = substr($tree[$key]['link']['link_path'], 5);
961       if (is_numeric($nid)) {
962         $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link'];
963         $tree[$key]['link']['access'] = FALSE;
964       }
965     }
966     if ($tree[$key]['below']) {
967       menu_tree_collect_node_links($tree[$key]['below'], $node_links);
968     }
969   }
970 }
971 
972 /**
973  * Check access and perform other dynamic operations for each link in the tree.
974  */
975 function menu_tree_check_access(&$tree, $node_links = array()) {
976 
977   if ($node_links) {
978     // Use db_rewrite_sql to evaluate view access without loading each full node.
979     $nids = array_keys($node_links);
980     $placeholders = '%d'. str_repeat(', %d', count($nids) - 1);
981     $result = db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.status = 1 AND n.nid IN (". $placeholders .")"), $nids);
982     while ($node = db_fetch_array($result)) {
983       $nid = $node['nid'];
984       foreach ($node_links[$nid] as $mlid => $link) {
985         $node_links[$nid][$mlid]['access'] = TRUE;
986       }
987     }
988   }
989   _menu_tree_check_access($tree);
990   return;
991 }
992 
993 /**
994  * Recursive helper function for menu_tree_check_access()
995  */
996 function _menu_tree_check_access(&$tree) {
997   $new_tree = array();
998   foreach ($tree as $key => $v) {
999     $item = &$tree[$key]['link'];
1000     _menu_link_translate($item);
1001     if ($item['access']) {
1002       if ($tree[$key]['below']) {
1003         _menu_tree_check_access($tree[$key]['below']);
1004       }
1005       // The weights are made a uniform 5 digits by adding 50000 as an offset.
1006       // After _menu_link_translate(), $item['title'] has the localized link title.
1007       // Adding the mlid to the end of the index insures that it is unique.
1008       $new_tree[(50000 + $item['weight']) .' '. $item['title'] .' '. $item['mlid']] = $tree[$key];
1009     }
1010   }
1011   // Sort siblings in the tree based on the weights and localized titles.
1012   ksort($new_tree);
1013   $tree = $new_tree;
1014 }
1015 
1016 /**
1017  * Build the data representing a menu tree.
1018  *
1019  * @param $result
1020  *   The database result.
1021  * @param $parents
1022  *   An array of the plid values that represent the path from the current page
1023  *   to the root of the menu tree.
1024  * @param $depth
1025  *   The depth of the current menu tree.
1026  * @return
1027  *   See menu_tree_page_data for a description of the data structure.
1028  */
1029 function menu_tree_data($result = NULL, $parents = array(), $depth = 1) {
1030   list(, $tree) = _menu_tree_data($result, $parents, $depth);
1031   return $tree;
1032 }
1033 
1034 /**
1035  * Recursive helper function to build the data representing a menu tree.
1036  *
1037  * The function is a bit complex because the rendering of an item depends on
1038  * the next menu item. So we are always rendering the element previously
1039  * processed not the current one.
1040  */
1041 function _menu_tree_data($result, $parents, $depth, $previous_element = '') {
1042   $remnant = NULL;
1043   $tree = array();
1044   while ($item = db_fetch_array($result)) {
1045     // We need to determine if we're on the path to root so we can later build
1046     // the correct active trail and breadcrumb.
1047     $item['in_active_trail'] = in_array($item['mlid'], $parents);
1048     // The current item is the first in a new submenu.
1049     if ($item['depth'] > $depth) {
1050       // _menu_tree returns an item and the menu tree structure.
1051       list($item, $below) = _menu_tree_data($result, $parents, $item['depth'], $item);
1052       if ($previous_element) {
1053         $tree[$previous_element['mlid']] = array(
1054           'link' => $previous_element,
1055           'below' => $below,
1056         );
1057       }
1058       else {
1059         $tree = $below;
1060       }
1061       // We need to fall back one level.
1062       if (!isset($item) || $item['depth'] < $depth) {
1063         return array($item, $tree);
1064       }
1065       // This will be the link to be output in the next iteration.
1066       $previous_element = $item;
1067     }
1068     // We are at the same depth, so we use the previous element.
1069     elseif ($item['depth'] == $depth) {
1070       if ($previous_element) {
1071         // Only the first time.
1072         $tree[$previous_element['mlid']] = array(
1073           'link' => $previous_element,
1074           'below' => FALSE,
1075         );
1076       }
1077       // This will be the link to be output in the next iteration.
1078       $previous_element = $item;
1079     }
1080     // The submenu ended with the previous item, so pass back the current item.
1081     else {
1082       $remnant = $item;
1083       break;
1084     }
1085   }
1086   if ($previous_element) {
1087     // We have one more link dangling.
1088     $tree[$previous_element['mlid']] = array(
1089       'link' => $previous_element,
1090       'below' => FALSE,
1091     );
1092   }
1093   return array($remnant, $tree);
1094 }
1095 
1096 /**
1097  * Generate the HTML output for a single menu link.
1098  *
1099  * @ingroup themeable
1100  */
1101 function theme_menu_item_link($link) {
1102   if (empty($link['localized_options'])) {
1103     $link['localized_options'] = array();
1104   }
1105 
1106   return l($link['title'], $link['href'], $link['localized_options']);
1107 }
1108 
1109 /**
1110  * Generate the HTML output for a menu tree
1111  *
1112  * @ingroup themeable
1113  */
1114 function theme_menu_tree($tree) {
1115   return '<ul class="menu">'. $tree .'</ul>';
1116 }
1117 
1118 /**
1119  * Generate the HTML output for a menu item and submenu.
1120  *
1121  * @ingroup themeable
1122  */
1123 function theme_menu_item($link, $has_children, $menu = '', $in_active_trail = FALSE, $extra_class = NULL) {
1124   $class = ($menu ? 'expanded' : ($has_children ? 'collapsed' : 'leaf'));
1125   if (!empty($extra_class)) {
1126     $class .= ' '. $extra_class;
1127   }
1128   if ($in_active_trail) {
1129     $class .= ' active-trail';
1130   }
1131   return '<li class="'. $class .'">'. $link . $menu ."</li>\n";
1132 }
1133 
1134 /**
1135  * Generate the HTML output for a single local task link.
1136  *
1137  * @ingroup themeable
1138  */
1139 function theme_menu_local_task($link, $active = FALSE) {
1140   return '<li '. ($active ? 'class="active" ' : '') .'>'. $link ."</li>\n";
1141 }
1142 
1143 /**
1144  * Generates elements for the $arg array in the help hook.
1145  */
1146 function drupal_help_arg($arg = array()) {
1147   // Note - the number of empty elements should be > MENU_MAX_PARTS.
1148   return $arg + array('', '', '', '', '', '', '', '', '', '', '', '');
1149 }
1150 
1151 /**
1152  * Returns the help associated with the active menu item.
1153  */
1154 function menu_get_active_help() {
1155   $output = '';
1156   $router_path = menu_tab_root_path();
1157 
1158   $arg = drupal_help_arg(arg(NULL));
1159   $empty_arg = drupal_help_arg();
1160 
1161   foreach (module_list() as $name) {
1162     if (module_hook($name, 'help')) {
1163       // Lookup help for this path.
1164       if ($help = module_invoke($name, 'help', $router_path, $arg)) {
1165         $output .= $help ."\n";
1166       }
1167       // Add "more help" link on admin pages if the module provides a
1168       // standalone help page.
1169       if ($arg[0] == "admin" && module_exists('help') && module_invoke($name, 'help', 'admin/help#'. $arg[2], $empty_arg) && $help) {
1170         $output .= theme("more_help_link", url('admin/help/'. $arg[2]));
1171       }
1172     }
1173   }
1174   return $output;
1175 }
1176 
1177 /**
1178  * Build a list of named menus.
1179  */
1180 function menu_get_names($reset = FALSE) {
1181   static $names;
1182 
1183   if ($reset || empty($names)) {
1184     $names = array();
1185     $result = db_query("SELECT DISTINCT(menu_name) FROM {menu_links} ORDER BY menu_name");
1186     while ($name = db_fetch_array($result)) {
1187       $names[] = $name['menu_name'];
1188     }
1189   }
1190   return $names;
1191 }
1192 
1193 /**
1194  * Return an array containing the names of system-defined (default) menus.
1195  */
1196 function menu_list_system_menus() {
1197   return array('navigation', 'primary-links', 'secondary-links');
1198 }
1199 
1200 /**
1201  * Return an array of links to be rendered as the Primary links.
1202  */
1203 function menu_primary_links() {
1204   return menu_navigation_links(variable_get('menu_primary_links_source', 'primary-links'));
1205 }
1206 
1207 /**
1208  * Return an array of links to be rendered as the Secondary links.
1209  */
1210 function menu_secondary_links() {
1211 
1212   // If the secondary menu source is set as the primary menu, we display the
1213   // second level of the primary menu.
1214   if (variable_get('menu_secondary_links_source', 'secondary-links') == variable_get('menu_primary_links_source', 'primary-links')) {
1215     return menu_navigation_links(variable_get('menu_primary_links_source', 'primary-links'), 1);
1216   }
1217   else {
1218     return menu_navigation_links(variable_get('menu_secondary_links_source', 'secondary-links'), 0);
1219   }
1220 }
1221 
1222 /**
1223  * Return an array of links for a navigation menu.
1224  *
1225  * @param $menu_name
1226  *   The name of the menu.
1227  * @param $level
1228  *   Optional, the depth of the menu to be returned.
1229  * @return
1230  *   An array of links of the specified menu and level.
1231  */
1232 function menu_navigation_links($menu_name, $level = 0) {
1233   // Don't even bother querying the menu table if no menu is specified.
1234   if (empty($menu_name)) {
1235     return array();
1236   }
1237 
1238   // Get the menu hierarchy for the current page.
1239   $tree = menu_tree_page_data($menu_name);
1240 
1241   // Go down the active trail until the right level is reached.
1242   while ($level-- > 0 && $tree) {
1243     // Loop through the current level's items until we find one that is in trail.
1244     while ($item = array_shift($tree)) {
1245       if ($item['link']['in_active_trail']) {
1246         // If the item is in the active trail, we continue in the subtree.
1247         $tree = empty($item['below']) ? array() : $item['below'];
1248         break;
1249       }
1250     }
1251   }
1252 
1253   // Create a single level of links.
1254   $links = array();
1255   foreach ($tree as $item) {
1256     if (!$item['link']['hidden']) {
1257       $l = $item['link']['localized_options'];
1258       $l['href'] = $item['link']['href'];
1259       $l['title'] = $item['link']['title'];
1260       // Keyed with unique menu id to generate classes from theme_links().
1261       $links['menu-'. $item['link']['mlid']] = $l;
1262     }
1263   }
1264   return $links;
1265 }
1266 
1267 /**
1268  * Collects the local tasks (tabs) for a given level.
1269  *
1270  * @param $level
1271  *   The level of tasks you ask for. Primary tasks are 0, secondary are 1.
1272  * @param $return_root
1273  *   Whether to return the root path for the current page.
1274  * @return
1275  *   Themed output corresponding to the tabs of the requested level, or
1276  *   router path if $return_root == TRUE. This router path corresponds to
1277  *   a parent tab, if the current page is a default local task.
1278  */
1279 function menu_local_tasks($level = 0, $return_root = FALSE) {
1280   static $tabs;
1281   static $root_path;
1282 
1283   if (!isset($tabs)) {
1284     $tabs = array();
1285 
1286     $router_item = menu_get_item();
1287     if (!$router_item || !$router_item['access']) {
1288       return '';
1289     }
1290     // Get all tabs and the root page.
1291     $result = db_query("SELECT * FROM {menu_router} WHERE tab_root = '%s' ORDER BY weight, title", $router_item['tab_root']);
1292     $map = arg();
1293     $children = array();
1294     $tasks = array();
1295     $root_path = $router_item['path'];
1296 
1297     while ($item = db_fetch_array($result)) {
1298       _menu_translate($item, $map, TRUE);
1299       if ($item['tab_parent']) {
1300         // All tabs, but not the root page.
1301         $children[$item['tab_parent']][$item['path']] = $item;
1302       }
1303       // Store the translated item for later use.
1304       $tasks[$item['path']] = $item;
1305     }
1306 
1307     // Find all tabs below the current path.
1308     $path = $router_item['path'];
1309     // Tab parenting may skip levels, so the number of parts in the path may not
1310     // equal the depth. Thus we use the $depth counter (offset by 1000 for ksort).
1311     $depth = 1001;
1312     while (isset($children[$path])) {
1313       $tabs_current = '';
1314       $next_path = '';
1315       $count = 0;
1316       foreach ($children[$path] as $item) {
1317         if ($item['access']) {
1318           $count++;
1319           // The default task is always active.
1320           if ($item['type'] == MENU_DEFAULT_LOCAL_TASK) {
1321             // Find the first parent which is not a default local task.
1322             for ($p = $item['tab_parent']; $tasks[$p]['type'] == MENU_DEFAULT_LOCAL_TASK; $p = $tasks[$p]['tab_parent']);
1323             $link = theme('menu_item_link', array('href' => $tasks[$p]['href']) + $item);
1324             $tabs_current .= theme('menu_local_task', $link, TRUE);
1325             $next_path = $item['path'];
1326           }
1327           else {
1328             $link = theme('menu_item_link', $item);
1329             $tabs_current .= theme('menu_local_task', $link);
1330           }
1331         }
1332       }
1333       $path = $next_path;
1334       $tabs[$depth]['count'] = $count;
1335       $tabs[$depth]['output'] = $tabs_current;
1336       $depth++;
1337     }
1338 
1339     // Find all tabs at the same level or above the current one.
1340     $parent = $router_item['tab_parent'];
1341     $path = $router_item['path'];
1342     $current = $router_item;
1343     $depth = 1000;
1344     while (isset($children[$parent])) {
1345       $tabs_current = '';
1346       $next_path = '';
1347       $next_parent = '';
1348       $count = 0;
1349       foreach ($children[$parent] as $item) {
1350         if ($item['access']) {
1351           $count++;
1352           if ($item['type'] == MENU_DEFAULT_LOCAL_TASK) {
1353             // Find the first parent which is not a default local task.
1354             for ($p = $item['tab_parent']; $tasks[$p]['type'] == MENU_DEFAULT_LOCAL_TASK; $p = $tasks[$p]['tab_parent']);
1355             $link = theme('menu_item_link', array('href' => $tasks[$p]['href']) + $item);
1356             if ($item['path'] == $router_item['path']) {
1357               $root_path = $tasks[$p]['path'];
1358             }
1359           }
1360           else {
1361             $link = theme('menu_item_link', $item);
1362           }
1363           // We check for the active tab.
1364           if ($item['path'] == $path) {
1365             $tabs_current .= theme('menu_local_task', $link, TRUE);
1366             $next_path = $item['tab_parent'];
1367             if (isset($tasks[$next_path])) {
1368               $next_parent = $tasks[$next_path]['tab_parent'];
1369             }
1370           }
1371           else {
1372             $tabs_current .= theme('menu_local_task', $link);
1373           }
1374         }
1375       }
1376       $path = $next_path;
1377       $parent = $next_parent;
1378       $tabs[$depth]['count'] = $count;
1379       $tabs[$depth]['output'] = $tabs_current;
1380       $depth--;
1381     }
1382     // Sort by depth.
1383     ksort($tabs);
1384     // Remove the depth, we are interested only in their relative placement.
1385     $tabs = array_values($tabs);
1386   }
1387 
1388   if ($return_root) {
1389     return $root_path;
1390   }
1391   else {
1392     // We do not display single tabs.
1393     return (isset($tabs[$level]) && $tabs[$level]['count'] > 1) ? $tabs[$level]['output'] : '';
1394   }
1395 }
1396 
1397 /**
1398  * Returns the rendered local tasks at the top level.
1399  */
1400 function menu_primary_local_tasks() {
1401   return menu_local_tasks(0);
1402 }
1403 
1404 /**
1405  * Returns the rendered local tasks at the second level.
1406  */
1407 function menu_secondary_local_tasks() {
1408   return menu_local_tasks(1);
1409 }
1410 
1411 /**
1412  * Returns the router path, or the path of the parent tab of a default local task.
1413  */
1414 function menu_tab_root_path() {
1415   return menu_local_tasks(0, TRUE);
1416 }
1417 
1418 /**
1419  * Returns the rendered local tasks. The default implementation renders them as tabs.
1420  *
1421  * @ingroup themeable
1422  */
1423 function theme_menu_local_tasks() {
1424   $output = '';
1425 
1426   if ($primary = menu_primary_local_tasks()) {
1427     $output .= "<ul class=\"tabs primary\">\n". $primary ."</ul>\n";
1428   }
1429   if ($secondary = menu_secondary_local_tasks()) {
1430     $output .= "<ul class=\"tabs secondary\">\n". $secondary ."</ul>\n";
1431   }
1432 
1433   return $output;
1434 }
1435 
1436 /**
1437  * Set (or get) the active menu for the current page - determines the active trail.
1438  */
1439 function menu_set_active_menu_name($menu_name = NULL) {
1440   static $active;
1441 
1442   if (isset($menu_name)) {
1443     $active = $menu_name;
1444   }
1445   elseif (!isset($active)) {
1446     $active = 'navigation';
1447   }
1448   return $active;
1449 }
1450 
1451 /**
1452  * Get the active menu for the current page - determines the active trail.
1453  */
1454 function menu_get_active_menu_name() {
1455   return menu_set_active_menu_name();
1456 }
1457 
1458 /**
1459  * Set the active path, which determines which page is loaded.
1460  *
1461  * @param $path
1462  *   A Drupal path - not a path alias.
1463  *
1464  * Note that this may not have the desired effect unless invoked very early
1465  * in the page load, such as during hook_boot, or unless you call
1466  * menu_execute_active_handler() to generate your page output.
1467  */
1468 function menu_set_active_item($path) {
1469   $_GET['q'] = $path;
1470 }
1471 
1472 /**
1473  * Set (or get) the active trail for the current page - the path to root in the menu tree.
1474  */
1475 function menu_set_active_trail($new_trail = NULL) {
1476   static $trail;
1477 
1478   if (isset($new_trail)) {
1479     $trail = $new_trail;
1480   }
1481   elseif (!isset($trail)) {
1482     $trail = array();
1483     $trail[] = array('title' => t('Home'), 'href' => '<front>', 'localized_options' => array(), 'type' => 0);
1484     $item = menu_get_item();
1485 
1486     // Check whether the current item is a local task (displayed as a tab).
1487     if ($item['tab_parent']) {
1488       // The title of a local task is used for the tab, never the page title.
1489       // Thus, replace it with the item corresponding to the root path to get
1490       // the relevant href and title.  For example, the menu item corresponding
1491       // to 'admin' is used when on the 'By module' tab at 'admin/by-module'.
1492       $parts = explode('/', $item['tab_root']);
1493       $args = arg();
1494       // Replace wildcards in the root path using the current path.
1495       foreach ($parts as $index => $part) {
1496         if ($part == '%') {
1497           $parts[$index] = $args[$index];
1498         }
1499       }
1500       // Retrieve the menu item using the root path after wildcard replacement.
1501       $root_item = menu_get_item(implode('/', $parts));
1502       if ($root_item && $root_item['access']) {
1503         $item = $root_item;
1504       }
1505     }
1506 
1507     $tree = menu_tree_page_data(menu_get_active_menu_name());
1508     list($key, $curr) = each($tree);
1509 
1510     while ($curr) {
1511       // Terminate the loop when we find the current path in the active trail.
1512       if ($curr['link']['href'] == $item['href']) {
1513         $trail[] = $curr['link'];
1514         $curr = FALSE;
1515       }
1516       else {
1517         // Move to the child link if it's in the active trail.
1518         if ($curr['below'] && $curr['link']['in_active_trail']) {
1519           $trail[] = $curr['link'];
1520           $tree = $curr['below'];
1521         }
1522         list($key, $curr) = each($tree);
1523       }
1524     }
1525     // Make sure the current page is in the trail (needed for the page title),
1526     // but exclude tabs and the front page.
1527     $last = count($trail) - 1;
1528     if ($trail[$last]['href'] != $item['href'] && !(bool)($item['type'] & MENU_IS_LOCAL_TASK) && !drupal_is_front_page()) {
1529       $trail[] = $item;
1530     }
1531   }
1532   return $trail;
1533 }
1534 
1535 /**
1536  * Get the active trail for the current page - the path to root in the menu tree.
1537  */
1538 function menu_get_active_trail() {
1539   return menu_set_active_trail();
1540 }
1541 
1542 /**
1543  * Get the breadcrumb for the current page, as determined by the active trail.
1544  */
1545 function menu_get_active_breadcrumb() {
1546   $breadcrumb = array();
1547 
1548   // No breadcrumb for the front page.
1549   if (drupal_is_front_page()) {
1550     return $breadcrumb;
1551   }
1552 
1553   $item = menu_get_item();
1554   if ($item && $item['access']) {
1555     $active_trail = menu_get_active_trail();
1556 
1557     foreach ($active_trail as $parent) {
1558       $breadcrumb[] = l($parent['title'], $parent['href'], $parent['localized_options']);
1559     }
1560     $end = end($active_trail);
1561 
1562     // Don't show a link to the current page in the breadcrumb trail.
1563     if ($item['href'] == $end['href'] || ($item['type'] == MENU_DEFAULT_LOCAL_TASK && $end['href'] != '<front>')) {
1564       array_pop($breadcrumb);
1565     }
1566   }
1567   return $breadcrumb;
1568 }
1569 
1570 /**
1571  * Get the title of the current page, as determined by the active trail.
1572  */
1573 function menu_get_active_title() {
1574   $active_trail = menu_get_active_trail();
1575 
1576   foreach (array_reverse($active_trail) as $item) {
1577     if (!(bool)($item['type'] & MENU_IS_LOCAL_TASK)) {
1578       return $item['title'];
1579     }
1580   }
1581 }
1582 
1583 /**
1584  * Get a menu link by its mlid, access checked and link translated for rendering.
1585  *
1586  * This function should never be called from within node_load() or any other
1587  * function used as a menu object load function since an infinite recursion may
1588  * occur.
1589  *
1590  * @param $mlid
1591  *   The mlid of the menu item.
1592  * @return
1593  *   A menu link, with $item['access'] filled and link translated for
1594  *   rendering.
1595  */
1596 function menu_link_load($mlid) {
1597   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))) {
1598     _menu_link_translate($item);
1599     return $item;
1600   }
1601   return FALSE;
1602 }
1603 
1604 /**
1605  * Clears the cached cached data for a single named menu.
1606  */
1607 function menu_cache_clear($menu_name = 'navigation') {
1608   static $cache_cleared = array();
1609 
16101  if (empty($cache_cleared[$menu_name])) {
16111    cache_clear_all('links:'. $menu_name .':', 'cache_menu', TRUE);
16121    $cache_cleared[$menu_name] = 1;
1613   }
16141  elseif ($cache_cleared[$menu_name] == 1) {
16151    register_shutdown_function('cache_clear_all', 'links:'. $menu_name .':', 'cache_menu', TRUE);
16161    $cache_cleared[$menu_name] = 2;
1617   }
1618 }
1619 
1620 /**
1621  * Clears all cached menu data.  This should be called any time broad changes
1622  * might have been made to the router items or menu links.
1623  */
1624 function menu_cache_clear_all() {
16251  cache_clear_all('*', 'cache_menu', TRUE);
1626 }
1627 
1628 /**
1629  * (Re)populate the database tables used by various menu functions.
1630  *
1631  * This function will clear and populate the {menu_router} table, add entries
1632  * to {menu_links} for new router items, then remove stale items from
1633  * {menu_links}. If called from update.php or install.php, it will also
1634  * schedule a call to itself on the first real page load from
1635  * menu_execute_active_handler(), because the maintenance page environment
1636  * is different and leaves stale data in the menu tables.
1637  */
1638 function menu_rebuild() {
16391  variable_del('menu_rebuild_needed');
16401  menu_cache_clear_all();
16411  $menu = menu_router_build(TRUE);
16421  _menu_navigation_links_rebuild($menu);
1643   // Clear the page and block caches.
16441  _menu_clear_page_cache();
16451  if (defined('MAINTENANCE_MODE')) {
1646     variable_set('menu_rebuild_needed', TRUE);
1647   }
1648 }
1649 
1650 /**
1651  * Collect, alter and store the menu definitions.
1652  */
1653 function menu_router_build($reset = FALSE) {
16541  static $menu;
1655 
16561  if (!isset($menu) || $reset) {
16571    if (!$reset && ($cache = cache_get('router:', 'cache_menu')) && isset($cache->data)) {
1658       $menu = $cache->data;
1659     }
1660     else {
16611      db_query('DELETE FROM {menu_router}');
1662       // We need to manually call each module so that we can know which module
1663       // a given item came from.
1664       $callbacks = array();
16651      foreach (module_implements('menu') as $module) {
16661        $router_items = call_user_func($module .'_menu');
16671        if (isset($router_items) && is_array($router_items)) {
16681          foreach (array_keys($router_items) as $path) {
16691            $router_items[$path]['module'] = $module;
1670           }
16711          $callbacks = array_merge($callbacks, $router_items);
1672         }
1673       }
1674       // Alter the menu as defined in modules, keys are like user/%user.
16751      drupal_alter('menu', $callbacks);
16761      $menu = _menu_router_build($callbacks);
16771      cache_set('router:', $menu, 'cache_menu');
1678     }
1679   }
16801  return $menu;
1681 }
1682 
1683 /**
1684  * Builds a link from a router item.
1685  */
1686 function _menu_link_build($item) {
16871  if ($item['type'] == MENU_CALLBACK) {
16881    $item['hidden'] = -1;
1689   }
16901  elseif ($item['type'] == MENU_SUGGESTED_ITEM) {
16911    $item['hidden'] = 1;
1692   }
1693   // Note, we set this as 'system', so that we can be sure to distinguish all
1694   // the menu links generated automatically from entries in {menu_router}.
16951  $item['module'] = 'system';
1696   $item += array(
16971    'menu_name' => 'navigation',
1698     'link_title' => $item['title'],
1699     'link_path' => $item['path'],
1700     'hidden' => 0,
1701     'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])),
1702   );
17031  return $item;
1704 }
1705 
1706 /**
1707  * Helper function to build menu links for the items in the menu router.
1708  */
1709 function _menu_navigation_links_rebuild($menu) {
1710   // Add normal and suggested items as links.
1711   $menu_links = array();
17121  foreach ($menu as $path => $item) {
17131    if ($item['_visible']) {
17141      $item = _menu_link_build($item);
17151      $menu_links[$path] = $item;
17161      $sort[$path] = $item['_number_parts'];
1717     }
1718   }
17191  if ($menu_links) {
1720     // Make sure no child comes before its parent.
17211    array_multisort($sort, SORT_NUMERIC, $menu_links);
1722 
17231    foreach ($menu_links as $item) {
17241      $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'));
17251      if ($existing_item) {
17261        $item['mlid'] = $existing_item['mlid'];
17271        $item['menu_name'] = $existing_item['menu_name'];
17281        $item['plid'] = $existing_item['plid'];
17291        $item['has_children'] = $existing_item['has_children'];
17301        $item['updated'] = $existing_item['updated'];
1731       }
17321      if (!$existing_item || !$existing_item['customized']) {
17331        menu_link_save($item);
1734       }
1735     }
1736   }
17371  $placeholders = db_placeholders($menu, 'varchar');
17381  $paths = array_keys($menu);
1739   // Updated and customized items whose router paths are gone need new ones.
17401  $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);
17411  while ($item = db_fetch_array($result)) {
1742     $router_path = _menu_find_router_path($menu, $item['link_path']);
1743     if (!empty($router_path) && ($router_path != $item['router_path'] || $item['updated'])) {
1744       // If the router path and the link path matches, it's surely a working
1745       // item, so we clear the updated flag.
1746       $updated = $item['updated'] && $router_path != $item['link_path'];
1747       db_query("UPDATE {menu_links} SET router_path = '%s', updated = %d WHERE mlid = %d", $router_path, $updated, $item['mlid']);
1748     }
1749   }
1750   // Find any item whose router path does not exist any more.
17511  $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);
1752   // Remove all such items. Starting from those with the greatest depth will
1753   // minimize the amount of re-parenting done by menu_link_delete().
17541  while ($item = db_fetch_array($result)) {
1755     _menu_delete_item($item, TRUE);
1756   }
1757 }
1758 
1759 /**
1760  * Delete one or several menu links.
1761  *
1762  * @param $mlid
1763  *   A valid menu link mlid or NULL. If NULL, $path is used.
1764  * @param $path
1765  *   The path to the menu items to be deleted. $mlid must be NULL.
1766  */
1767 function menu_link_delete($mlid, $path = NULL) {
1768   if (isset($mlid)) {
1769     _menu_delete_item(db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $mlid)));
1770   }
1771   else {
1772     $result = db_query("SELECT * FROM {menu_links} WHERE link_path = '%s'", $path);
1773     while ($link = db_fetch_array($result)) {
1774       _menu_delete_item($link);
1775     }
1776   }
1777 }
1778 
1779 /**
1780  * Helper function for menu_link_delete; deletes a single menu link.
1781  *
1782  * @param $item
1783  *   Item to be deleted.
1784  * @param $force
1785  *   Forces deletion. Internal use only, setting to TRUE is discouraged.
1786  */
1787 function _menu_delete_item($item, $force = FALSE) {
1788   if ($item && ($item['module'] != 'system' || $item['updated'] || $force)) {
1789     // Children get re-attached to the item's parent.
1790     if ($item['has_children']) {
1791       $result = db_query("SELECT mlid FROM {menu_links} WHERE plid = %d", $item['mlid']);
1792       while ($m = db_fetch_array($result)) {
1793         $child = menu_link_load($m['mlid']);
1794         $child['plid'] = $item['plid'];
1795         menu_link_save($child);
1796       }
1797     }
1798     db_query('DELETE FROM {menu_links} WHERE mlid = %d', $item['mlid']);
1799 
1800     // Update the has_children status of the parent.
1801     _menu_update_parental_status($item);
1802     menu_cache_clear($item['menu_name']);
1803     _menu_clear_page_cache();
1804   }
1805 }
1806 
1807 /**
1808  * Save a menu link.
1809  *
1810  * @param $item
1811  *   An array representing a menu link item. The only mandatory keys are
1812  *   link_path and link_title. Possible keys are:
1813  *   - menu_name   default is navigation
1814  *   - weight      default is 0
1815  *   - expanded    whether the item is expanded.
1816  *   - options     An array of options, @see l for more.
1817  *   - mlid        Set to an existing value, or 0 or NULL to insert a new link.
1818  *   - plid        The mlid of the parent.
1819  *   - router_path The path of the relevant router item.
1820  */
1821 function menu_link_save(&$item) {
18221  $menu = menu_router_build();
1823 
18241  drupal_alter('menu_link', $item, $menu);
1825 
1826   // This is the easiest way to handle the unique internal path '<front>',
1827   // since a path marked as external does not need to match a router path.
18281  $item['_external'] = menu_path_is_external($item['link_path'])  || $item['link_path'] == '<front>';
1829   // Load defaults.
1830   $item += array(
18311    'menu_name' => 'navigation',
1832     'weight' => 0,
1833     'link_title' => '',
1834     'hidden' => 0,
1835     'has_children' => 0,
1836     'expanded' => 0,
1837     'options' => array(),
1838     'module' => 'menu',
1839     'customized' => 0,
1840     'updated' => 0,
1841   );
18421  $existing_item = FALSE;
18431  if (isset($item['mlid'])) {
18441    $existing_item = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $item['mlid']));
1845   }
1846 
18471  if (isset($item['plid'])) {
18481    $parent = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $item['plid']));
1849   }
1850   else {
1851     // Find the parent - it must be unique.
18521    $parent_path = $item['link_path'];
18531    $where = "WHERE link_path = '%s'";
1854     // Only links derived from router items should have module == 'system', and
1855     // we want to find the parent even if it's in a different menu.
18561    if ($item['module'] == 'system') {
18571      $where .= " AND module = '%s'";
18581      $arg2 = 'system';
1859     }
1860     else {
1861       // If not derived from a router item, we respect the specified menu name.
1862       $where .= " AND menu_name = '%s'";
1863       $arg2 = $item['menu_name'];
1864     }
1865     do {
18661      $parent = FALSE;
18671      $parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
18681      $result = db_query("SELECT COUNT(*) FROM {menu_links} ". $where, $parent_path, $arg2);
1869       // Only valid if we get a unique result.
18701      if (db_result($result) == 1) {
18711        $parent = db_fetch_array(db_query("SELECT * FROM {menu_links} ". $where, $parent_path, $arg2));
1872       }
18731    } while ($parent === FALSE && $parent_path);
1874   }
18751  if ($parent !== FALSE) {
18761    $item['menu_name'] = $parent['menu_name'];
1877   }
18781  $menu_name = $item['menu_name'];
1879   // Menu callbacks need to be in the links table for breadcrumbs, but can't
1880   // be parents if they are generated directly from a router item.
18811  if (empty($parent['mlid']) || $parent['hidden'] < 0) {
18821    $item['plid'] =  0;
1883   }
1884   else {
18851    $item['plid'] = $parent['mlid'];
1886   }
1887 
18881  if (!$existing_item) {
18891    db_query("INSERT INTO {menu_links} (
1890        menu_name, plid, link_path,
1891       hidden, external, has_children,
1892       expanded, weight,
1893       module, link_title, options,
1894       customized, updated) VALUES (
1895       '%s', %d, '%s',
1896       %d, %d, %d,
1897       %d, %d,
18981      '%s', '%s', '%s', %d, %d)",
18991      $item['menu_name'], $item['plid'], $item['link_path'],
1900       $item['hidden'], $item['_external'], $item['has_children'],
1901       $item['expanded'], $item['weight'],
1902       $item['module'],  $item['link_title'], serialize($item['options']),
1903       $item['customized'], $item['updated']);
19041    $item['mlid'] = db_last_insert_id('menu_links', 'mlid');
1905   }
1906 
19071  if (!$item['plid']) {
19081    $item['p1'] = $item['mlid'];
19091    for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
19101      $item["p$i"] = 0;
1911     }
19121    $item['depth'] = 1;
1913   }
1914   else {
1915     // Cannot add beyond the maximum depth.
19161    if ($item['has_children'] && $existing_item) {
19171      $limit = MENU_MAX_DEPTH - menu_link_children_relative_depth($existing_item) - 1;
1918     }
1919     else {
19201      $limit = MENU_MAX_DEPTH - 1;
1921     }
19221    if ($parent['depth'] > $limit) {
1923       return FALSE;
1924     }
19251    $item['depth'] = $parent['depth'] + 1;
19261    _menu_link_parents_set($item, $parent);
1927   }
1928   // Need to check both plid and menu_name, since plid can be 0 in any menu.
19291  if ($existing_item && ($item['plid'] != $existing_item['plid'] || $menu_name != $existing_item['menu_name'])) {
1930     _menu_link_move_children($item, $existing_item);
1931   }
1932   // Find the callback. During the menu update we store empty paths to be
1933   // fixed later, so we skip this.
19341  if (!isset($_SESSION['system_update_6021']) && (empty($item['router_path'])  || !$existing_item || ($existing_item['link_path'] != $item['link_path']))) {
19351    if ($item['_external']) {
1936       $item['router_path'] = '';
1937     }
1938     else {
1939       // Find the router path which will serve this path.
19401      $item['parts'] = explode('/', $item['link_path'], MENU_MAX_PARTS);
19411      $item['router_path'] = _menu_find_router_path($menu, $item['link_path']);
1942     }
1943   }
19441  db_query("UPDATE {menu_links} SET menu_name = '%s', plid = %d, link_path = '%s',
1945     router_path = '%s', hidden = %d, external = %d, has_children = %d,
1946     expanded = %d, weight = %d, depth = %d,
1947     p1 = %d, p2 = %d, p3 = %d, p4 = %d, p5 = %d, p6 = %d, p7 = %d, p8 = %d, p9 = %d,
19481    module = '%s', link_title = '%s', options = '%s', customized = %d WHERE mlid = %d",
19491    $item['menu_name'], $item['plid'], $item['link_path'],
1950     $item['router_path'], $item['hidden'], $item['_external'], $item['has_children'],
1951     $item['expanded'], $item['weight'],  $item['depth'],
1952     $item['p1'], $item['p2'], $item['p3'], $item['p4'], $item['p5'], $item['p6'], $item['p7'], $item['p8'], $item['p9'],
1953     $item['module'],  $item['link_title'], serialize($item['options']), $item['customized'], $item['mlid']);
1954   // Check the has_children status of the parent.
19551  _menu_update_parental_status($item);
19561  menu_cache_clear($menu_name);
19571  if ($existing_item && $menu_name != $existing_item['menu_name']) {
1958     menu_cache_clear($existing_item['menu_name']);
1959   }
1960 
19611  _menu_clear_page_cache();
19621  return $item['mlid'];
1963 }
1964 
1965 /**
1966  * Helper function to clear the page and block caches at most twice per page load.
1967  */
1968 function _menu_clear_page_cache() {
19691  static $cache_cleared = 0;
1970 
1971   // Clear the page and block caches, but at most twice, including at
1972   //  the end of the page load when there are multple links saved or deleted.
19731  if (empty($cache_cleared)) {
19741    cache_clear_all();
1975     // Keep track of which menus have expanded items.
19761    _menu_set_expanded_menus();
19771    $cache_cleared = 1;
1978   }
19791  elseif ($cache_cleared == 1) {
19801    register_shutdown_function('cache_clear_all');
1981     // Keep track of which menus have expanded items.
19821    register_shutdown_function('_menu_set_expanded_menus');
19831    $cache_cleared = 2;
1984   }
1985 }
1986 
1987 /**
1988  * Helper function to update a list of menus with expanded items
1989  */
1990 function _menu_set_expanded_menus() {
1991   $names = array();
19921  $result = db_query("SELECT menu_name FROM {menu_links} WHERE expanded != 0 GROUP BY menu_name");
19931  while ($n = db_fetch_array($result)) {
1994     $names[] = $n['menu_name'];
1995   }
19961  variable_set('menu_expanded', $names);
1997 }
1998 
1999 /**
2000  * Find the router path which will serve this path.
2001  *
2002  * @param $menu
2003  *  The full built menu.
2004  * @param $link_path
2005  *  The path for we are looking up its router path.
2006  * @return
2007  *  A path from $menu keys or empty if $link_path points to a nonexisting
2008  *  place.
2009  */
2010 function _menu_find_router_path($menu, $link_path) {
20111  $parts = explode('/', $link_path, MENU_MAX_PARTS);
20121  $router_path = $link_path;
20131  if (!isset($menu[$router_path])) {
20141    list($ancestors) = menu_get_ancestors($parts);
20151    $ancestors[] = '';
20161    foreach ($ancestors as $key => $router_path) {
20171      if (isset($menu[$router_path])) {
20181        break;
2019       }
2020     }
2021   }
20221  return $router_path;
2023 }
2024 
2025 /**
2026  * Insert, update or delete an uncustomized menu link related to a module.
2027  *
2028  * @param $module
2029  *   The name of the module.
2030  * @param $op
2031  *   Operation to perform: insert, update or delete.
2032  * @param $link_path
2033  *   The path this link points to.
2034  * @param $link_title
2035  *   Title of the link to insert or new title to update the link to.
2036  *   Unused for delete.
2037  * @return
2038  *   The insert op returns the mlid of the new item. Others op return NULL.
2039  */
2040 function menu_link_maintain($module, $op, $link_path, $link_title) {
2041   switch ($op) {
2042     case 'insert':
2043       $menu_link = array(
2044         'link_title' => $link_title,
2045         'link_path' => $link_path,
2046         'module' => $module,
2047       );
2048       return menu_link_save($menu_link);
2049       break;
2050     case 'update':
2051       db_query("UPDATE {menu_links} SET link_title = '%s' WHERE link_path = '%s' AND customized = 0 AND module = '%s'", $link_title, $link_path, $module);
2052       menu_cache_clear();
2053       break;
2054     case 'delete':
2055       menu_link_delete(NULL, $link_path);
2056       break;
2057   }
2058 }
2059 
2060 /**
2061  * Find the depth of an item's children relative to its depth.
2062  *
2063  * For example, if the item has a depth of 2, and the maximum of any child in
2064  * the menu link tree is 5, the relative depth is 3.
2065  *
2066  * @param $item
2067  *   An array representing a menu link item.
2068  * @return
2069  *   The relative depth, or zero.
2070  *
2071  */
2072 function menu_link_children_relative_depth($item) {
20731  $i = 1;
20741  $match = '';
20751  $args[] = $item['menu_name'];
20761  $p = 'p1';
20771  while ($i <= MENU_MAX_DEPTH && $item[$p]) {
20781    $match .= " AND $p = %d";
20791    $args[] = $item[$p];
20801    $p = 'p'. ++$i;
2081   }
2082 
20831  $max_depth = db_result(db_query_range("SELECT depth FROM {menu_links} WHERE menu_name = '%s'". $match ." ORDER BY depth DESC", $args, 0, 1));
2084 
20851  return ($max_depth > $item['depth']) ? $max_depth - $item['depth'] : 0;
2086 }
2087 
2088 /**
2089  * Update the children of a menu link that's being moved.
2090  *
2091  * The menu name, parents (p1 - p6), and depth are updated for all children of
2092  * the link, and the has_children status of the previous parent is updated.
2093  */
2094 function _menu_link_move_children($item, $existing_item) {
2095 
2096   $args[] = $item['menu_name'];
2097   $set[] = "menu_name = '%s'";
2098 
2099   $i = 1;
2100   while ($i <= $item['depth']) {
2101     $p = 'p'. $i++;
2102     $set[] = "$p = %d";
2103     $args[] = $item[$p];
2104   }
2105   $j = $existing_item['depth'] + 1;
2106   while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) {
2107     $set[] = 'p'. $i++ .' = p'. $j++;
2108   }
2109   while ($i <= MENU_MAX_DEPTH) {
2110     $set[] = 'p'. $i++ .' = 0';
2111   }
2112 
2113   $shift = $item['depth'] - $existing_item['depth'];
2114   if ($shift < 0) {
2115     $args[] = -$shift;
2116     $set[] = 'depth = depth - %d';
2117   }
2118   elseif ($shift > 0) {
2119     // The order of $set must be reversed so the new values don't overwrite the
2120     // old ones before they can be used because "Single-table UPDATE
2121     // assignments are generally evaluated from left to right"
2122     // see: http://dev.mysql.com/doc/refman/5.0/en/update.html
2123     $set = array_reverse($set);
2124     $args = array_reverse($args);
2125 
2126     $args[] = $shift;
2127     $set[] = 'depth = depth + %d';
2128   }
2129   $where[] = "menu_name = '%s'";
2130   $args[] = $existing_item['menu_name'];
2131   $p = 'p1';
2132   for ($i = 1; $i <= MENU_MAX_DEPTH && $existing_item[$p]; $p = 'p'. ++$i) {
2133     $where[] = "$p = %d";
2134     $args[] = $existing_item[$p];
2135   }
2136 
2137   db_query("UPDATE {menu_links} SET ". implode(', ', $set) ." WHERE ". implode(' AND ', $where), $args);
2138   // Check the has_children status of the parent, while excluding this item.
2139   _menu_update_parental_status($existing_item, TRUE);
2140 }
2141 
2142 /**
2143  * Check and update the has_children status for the parent of a link.
2144  */
2145 function _menu_update_parental_status($item, $exclude = FALSE) {
2146   // If plid == 0, there is nothing to update.
21471  if ($item['plid']) {
2148     // We may want to exclude the passed link as a possible child.
21491    $where = $exclude ? " AND mlid != %d" : '';
2150     // Check if at least one visible child exists in the table.
21511    $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));
21521    db_query("UPDATE {menu_links} SET has_children = %d WHERE mlid = %d", $parent_has_children, $item['plid']);
2153   }
2154 }
2155 
2156 /**
2157  * Helper function that sets the p1..p9 values for a menu link being saved.
2158  */
2159 function _menu_link_parents_set(&$item, $parent) {
21601  $i = 1;
21611  while ($i < $item['depth']) {
21621    $p = 'p'. $i++;
21631    $item[$p] = $parent[$p];
2164   }
21651  $p = 'p'. $i++;
2166   // The parent (p1 - p9) corresponding to the depth always equals the mlid.
21671  $item[$p] = $item['mlid'];
21681  while ($i <= MENU_MAX_DEPTH) {
21691    $p = 'p'. $i++;
21701    $item[$p] = 0;
2171   }
2172 }
2173 
2174 /**
2175  * Helper function to build the router table based on the data from hook_menu.
2176  */
2177 function _menu_router_build($callbacks) {
2178   // First pass: separate callbacks from paths, making paths ready for
2179   // matching. Calculate fitness, and fill some default values.
2180   $menu = array();
21811  foreach ($callbacks as $path => $item) {
2182     $load_functions = array();
2183     $to_arg_functions = array();
21841    $fit = 0;
21851    $move = FALSE;
2186 
21871    $parts = explode('/', $path, MENU_MAX_PARTS);
21881    $number_parts = count($parts);
2189     // We store the highest index of parts here to save some work in the fit
2190     // calculation loop.
21911    $slashes = $number_parts - 1;
2192     // Extract load and to_arg functions.
21931    foreach ($parts as $k => $part) {
21941      $match = FALSE;
21951      if (preg_match('/^%([a-z_]*)$/', $part, $matches)) {
21961        if (empty($matches[1])) {
21971          $match = TRUE;
21981          $load_functions[$k] = NULL;
2199         }
2200         else {
22011          if (function_exists($matches[1] .'_to_arg')) {
22021            $to_arg_functions[$k] = $matches[1] .'_to_arg';
22031            $load_functions[$k] = NULL;
22041            $match = TRUE;
2205           }
22061          if (function_exists($matches[1] .'_load')) {
22071            $function = $matches[1] .'_load';
2208             // Create an array of arguments that will be passed to the _load
2209             // function when this menu path is checked, if 'load arguments'
2210             // exists.
22111            $load_functions[$k] = isset($item['load arguments']) ? array($function => $item['load arguments']) : $function;
22121            $match = TRUE;
2213           }
2214         }
2215       }
22161      if ($match) {
22171        $parts[$k] = '%';
2218       }
2219       else {
22201        $fit |=  1 << ($slashes - $k);
2221       }
2222     }
22231    if ($fit) {
22241      $move = TRUE;
2225     }
2226     else {
2227       // If there is no %, it fits maximally.
2228       $fit = (1 << $number_parts) - 1;
2229     }
22301    $masks[$fit] = 1;
22311    $item['load_functions'] = empty($load_functions) ? '' : serialize($load_functions);
22321    $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions);
2233     $item += array(
22341      'title' => '',
2235       'weight' => 0,
2236       'type' => MENU_NORMAL_ITEM,
2237       '_number_parts' => $number_parts,
2238       '_parts' => $parts,
2239       '_fit' => $fit,
2240     );
2241     $item += array(
22421      '_visible' => (bool)($item['type'] & MENU_VISIBLE_IN_BREADCRUMB),
2243       '_tab' => (bool)($item['type'] & MENU_IS_LOCAL_TASK),
2244     );
22451    if ($move) {
22461      $new_path = implode('/', $item['_parts']);
22471      $menu[$new_path] = $item;
22481      $sort[$new_path] = $number_parts;
2249     }
2250     else {
2251       $menu[$path] = $item;
2252       $sort[$path] = $number_parts;
2253     }
2254   }
22551  array_multisort($sort, SORT_NUMERIC, $menu);
2256 
2257   // Apply inheritance rules.
22581  foreach ($menu as $path => $v) {
22591    $item = &$menu[$path];
22601    if (!$item['_tab']) {
2261       // Non-tab items.
22621      $item['tab_parent'] = '';
22631      $item['tab_root'] = $path;
2264     }
22651    for ($i = $item['_number_parts'] - 1; $i; $i--) {
22661      $parent_path = implode('/', array_slice($item['_parts'], 0, $i));
22671      if (isset($menu[$parent_path])) {
2268 
22691        $parent = $menu[$parent_path];
2270 
22711        if (!isset($item['tab_parent'])) {
2272           // Parent stores the parent of the path.
22731          $item['tab_parent'] = $parent_path;
2274         }
22751        if (!isset($item['tab_root']) && !$parent['_tab']) {
22761          $item['tab_root'] = $parent_path;
2277         }
2278         // If a callback is not found, we try to find the first parent that
2279         // has a callback.
22801        if (!isset($item['access callback']) && isset($parent['access callback'])) {
22811          $item['access callback'] = $parent['access callback'];
22821          if (!isset($item['access arguments']) && isset($parent['access arguments'])) {
22831            $item['access arguments'] = $parent['access arguments'];
2284           }
2285         }
2286         // Same for page callbacks.
22871        if (!isset($item['page callback']) && isset($parent['page callback'])) {
22881          $item['page callback'] = $parent['page callback'];
22891          if (!isset($item['page arguments']) && isset($parent['page arguments'])) {
22901            $item['page arguments'] = $parent['page arguments'];
2291           }
22921          if (!isset($item['file']) && isset($parent['file'])) {
22931            $item['file'] = $parent['file'];
2294           }
22951          if (!isset($item['file path']) && isset($parent['file path'])) {
22961            $item['file path'] = $parent['file path'];
2297           }
2298         }
2299       }
2300     }
23011    if (!isset($item['access callback']) && isset($item['access arguments'])) {
2302       // Default callback.
23031      $item['access callback'] = 'user_access';
2304     }
23051    if (!isset($item['access callback']) || empty($item['page callback'])) {
2306       $item['access callback'] = 0;
2307     }
23081    if (is_bool($item['access callback'])) {
23091      $item['access callback'] = intval($item['access callback']);
2310     }
2311 
2312     $item += array(
23131      'access arguments' => array(),
2314       'access callback' => '',
2315       'page arguments' => array(),
2316       'page callback' => '',
2317       'block callback' => '',
2318       'title arguments' => array(),
2319       'title callback' => 't',
2320       'description' => '',
2321       'position' => '',
2322       'tab_parent' => '',
2323       'tab_root' => $path,
2324       'path' => $path,
2325       'file' => '',
2326       'file path' => '',
2327       'include file' => '',
2328     );
2329 
2330     // Calculate out the file to be included for each callback, if any.
23311    if ($item['file']) {
23321      $file_path = $item['file path'] ? $item['file path'] : drupal_get_path('module', $item['module']);
23331      $item['include file'] = $file_path .'/'. $item['file'];
2334     }
2335 
23361    $title_arguments = $item['title arguments'] ? serialize($item['title arguments']) : '';
23371    db_query("INSERT INTO {menu_router}
2338       (path, load_functions, to_arg_functions, access_callback,
2339       access_arguments, page_callback, page_arguments, fit,
2340       number_parts, tab_parent, tab_root,
2341       title, title_callback, title_arguments,
2342       type, block_callback, description, position, weight, file)
2343       VALUES ('%s', '%s', '%s', '%s',
2344       '%s', '%s', '%s', %d,
2345       %d, '%s', '%s',
2346       '%s', '%s', '%s',
23471      %d, '%s', '%s', '%s', %d, '%s')",
23481      $path, $item['load_functions'], $item['to_arg_functions'], $item['access callback'],
2349       serialize($item['access arguments']), $item['page callback'], serialize($item['page arguments']), $item['_fit'],
2350       $item['_number_parts'], $item['tab_parent'], $item['tab_root'],
2351       $item['title'], $item['title callback'], $title_arguments,
2352       $item['type'], $item['block callback'], $item['description'], $item['position'], $item['weight'], $item['include file']);
2353   }
2354   // Sort the masks so they are in order of descending fit, and store them.
23551  $masks = array_keys($masks);
23561  rsort($masks);
23571  variable_set('menu_masks', $masks);
23581  return $menu;
2359 }
2360 
2361 /**
2362  * Returns TRUE if a path is external (e.g. http://example.com).
2363  */
2364 function menu_path_is_external($path) {
23651  $colonpos = strpos($path, ':');
23661  return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && filter_xss_bad_protocol($path, FALSE) == check_plain($path);
2367 }
2368 
2369 /**
2370  * Checks whether the site is off-line for maintenance.
2371  *
2372  * This function will log the current user out and redirect to front page
2373  * if the current user has no 'administer site configuration' permission.
2374  *
2375  * @return
2376  *   FALSE if the site is not off-line or its the login page or the user has
2377  *     'administer site configuration' permission.
2378  *   TRUE for anonymous users not on the login page if the site is off-line.
2379  */
2380 function _menu_site_is_offline() {
2381   // Check if site is set to off-line mode.
2382   if (variable_get('site_offline', 0)) {
2383     // Check if the user has administration privileges.
2384     if (user_access('administer site configuration')) {
2385       // Ensure that the off-line message is displayed only once [allowing for
2386       // page redirects], and specifically suppress its display on the site
2387       // maintenance page.
2388       if (drupal_get_normal_path($_GET['q']) != 'admin/settings/site-maintenance') {
2389         drupal_set_message(t('Operating in off-line mode.'), 'status', FALSE);
2390       }
2391     }
2392     else {
2393       // Anonymous users get a FALSE at the login prompt, TRUE otherwise.
2394       if (user_is_anonymous()) {
2395         return $_GET['q'] != 'user' && $_GET['q'] != 'user/login';
2396       }
2397       // Logged in users are unprivileged here, so they are logged out.
2398       require_once drupal_get_path('module', 'user') .'/user.pages.inc';
2399       user_logout();
2400     }
2401   }
2402   return FALSE;
2403 }
2404 
2405 /**
2406  * Validates the path of a menu link being created or edited.
2407  *
2408  * @return
2409  *   TRUE if it is a valid path AND the current user has access permission,
2410  *   FALSE otherwise.
2411  */
2412 function menu_valid_path($form_item) {
2413   global $menu_admin;
2414   $item = array();
2415   $path = $form_item['link_path'];
2416   // We indicate that a menu administrator is running the menu access check.
2417   $menu_admin = TRUE;
2418   if ($path == '<front>' || menu_path_is_external($path)) {
2419     $item = array('access' => TRUE);
2420   }
2421   elseif (preg_match('/\/\%/', $path)) {
2422     // Path is dynamic (ie 'user/%'), so check directly against menu_router table.
2423     if ($item = db_fetch_array(db_query("SELECT * FROM {menu_router} where path = '%s' ", $path))) {
2424       $item['link_path']  = $form_item['link_path'];
2425       $item['link_title'] = $form_item['link_title'];
2426       $item['external']   = FALSE;
2427       $item['options'] = '';
2428       _menu_link_translate($item);
2429     }
2430   }
2431   else {
2432     $item = menu_get_item($path);
2433   }
2434   $menu_admin = FALSE;
2435   return $item && $item['access'];
2436 }
2437 
2438 /**
2439  * @} End of "defgroup menu".
2440  */