specfile
[plewww.git] / includes / menu.inc
1 <?php
2 // $Id: menu.inc 144 2007-03-28 07:52:20Z thierry $
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 "callback 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" attribute of each menu item is checked as the search for a
51  * callback proceeds. If this attribute is TRUE, then access is granted; if
52  * FALSE, then access is denied. The first found "access" attribute
53  * determines the accessibility of the target. Menu items may omit this
54  * attribute to use the value provided by an ancestor item.
55  *
56  * In the default Drupal interface, you will notice many links rendered as
57  * tabs. These are known in the menu system as "local tasks", and they are
58  * rendered as tabs by default, though other presentations are possible.
59  * Local tasks function just as other menu items in most respects. It is
60  * convention that the names of these tasks should be short verbs if
61  * possible. In addition, a "default" local task should be provided for
62  * each set. When visiting a local task's parent menu item, the default
63  * local task will be rendered as if it is selected; this provides for a
64  * normal tab user experience. This default task is special in that it
65  * links not to its provided path, but to its parent item's path instead.
66  * The default task's path is only used to place it appropriately in the
67  * menu hierarchy.
68  */
69
70 /**
71  * @name Menu flags
72  * @{
73  * Flags for use in the "type" attribute of menu items.
74  */
75
76 define('MENU_IS_ROOT', 0x0001);
77 define('MENU_VISIBLE_IN_TREE', 0x0002);
78 define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004);
79 define('MENU_VISIBLE_IF_HAS_CHILDREN', 0x0008);
80 define('MENU_MODIFIABLE_BY_ADMIN', 0x0010);
81 define('MENU_MODIFIED_BY_ADMIN', 0x0020);
82 define('MENU_CREATED_BY_ADMIN', 0x0040);
83 define('MENU_IS_LOCAL_TASK', 0x0080);
84 define('MENU_EXPANDED', 0x0100);
85 define('MENU_LINKS_TO_PARENT', 0x0200);
86
87 /**
88  * @} End of "Menu flags".
89  */
90
91 /**
92  * @name Menu item types
93  * @{
94  * Menu item definitions provide one of these constants, which are shortcuts for
95  * combinations of the above flags.
96  */
97
98 /**
99  * Normal menu items show up in the menu tree and can be moved/hidden by
100  * the administrator. Use this for most menu items. It is the default value if
101  * no menu item type is specified.
102  */
103 define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB | MENU_MODIFIABLE_BY_ADMIN);
104
105 /**
106  * Item groupings are used for pages like "node/add" that simply list
107  * subpages to visit. They are distinguished from other pages in that they will
108  * disappear from the menu if no subpages exist.
109  */
110 define('MENU_ITEM_GROUPING', MENU_VISIBLE_IF_HAS_CHILDREN | MENU_VISIBLE_IN_BREADCRUMB | MENU_MODIFIABLE_BY_ADMIN);
111
112 /**
113  * Callbacks simply register a path so that the correct function is fired
114  * when the URL is accessed. They are not shown in the menu.
115  */
116 define('MENU_CALLBACK', MENU_VISIBLE_IN_BREADCRUMB);
117
118 /**
119  * Dynamic menu items change frequently, and so should not be stored in the
120  * database for administrative customization.
121  */
122 define('MENU_DYNAMIC_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);
123
124 /**
125  * Modules may "suggest" menu items that the administrator may enable. They act
126  * just as callbacks do until enabled, at which time they act like normal items.
127  */
128 define('MENU_SUGGESTED_ITEM', MENU_MODIFIABLE_BY_ADMIN | MENU_VISIBLE_IN_BREADCRUMB);
129
130 /**
131  * Local tasks are rendered as tabs by default. Use this for menu items that
132  * describe actions to be performed on their parent item. An example is the path
133  * "node/52/edit", which performs the "edit" task on "node/52".
134  */
135 define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK);
136
137 /**
138  * Every set of local tasks should provide one "default" task, that links to the
139  * same path as its parent when clicked.
140  */
141 define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT);
142
143 /**
144  * Custom items are those defined by the administrator. Reserved for internal
145  * use; do not return from hook_menu() implementations.
146  */
147 define('MENU_CUSTOM_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB | MENU_CREATED_BY_ADMIN | MENU_MODIFIABLE_BY_ADMIN);
148
149 /**
150  * Custom menus are those defined by the administrator. Reserved for internal
151  * use; do not return from hook_menu() implementations.
152  */
153 define('MENU_CUSTOM_MENU', MENU_IS_ROOT | MENU_VISIBLE_IN_TREE | MENU_CREATED_BY_ADMIN | MENU_MODIFIABLE_BY_ADMIN);
154
155 /**
156  * @} End of "Menu item types".
157  */
158
159 /**
160  * @name Menu status codes
161  * @{
162  * Status codes for menu callbacks.
163  */
164
165 define('MENU_FOUND', 1);
166 define('MENU_NOT_FOUND', 2);
167 define('MENU_ACCESS_DENIED', 3);
168 define('MENU_SITE_OFFLINE', 4);
169
170 /**
171  * @} End of "Menu status codes".
172  */
173
174 /**
175  * Return the menu data structure.
176  *
177  * The returned structure contains much information that is useful only
178  * internally in the menu system. External modules are likely to need only
179  * the ['visible'] element of the returned array. All menu items that are
180  * accessible to the current user and not hidden will be present here, so
181  * modules and themes can use this structure to build their own representations
182  * of the menu.
183  *
184  * $menu['visible'] will contain an associative array, the keys of which
185  * are menu IDs. The values of this array are themselves associative arrays,
186  * with the following key-value pairs defined:
187  * - 'title' - The displayed title of the menu or menu item. It will already
188  *   have been translated by the locale system.
189  * - 'description' - The description (link title attribute) of the menu item.
190  *   It will already have been translated by the locale system.
191  * - 'path' - The Drupal path to the menu item. A link to a particular item
192  *   can thus be constructed with
193  *   l($item['title'], $item['path'], array('title' => $item['description'])).
194  * - 'children' - A linear list of the menu ID's of this item's children.
195  *
196  * Menu ID 0 is the "root" of the menu. The children of this item are the
197  * menus themselves (they will have no associated path). Menu ID 1 will
198  * always be one of these children; it is the default "Navigation" menu.
199  */
200 function menu_get_menu() {
201   global $_menu;
202   global $user;
203   global $locale;
204
205   if (!isset($_menu['items'])) {
206     // _menu_build() may indirectly call this function, so prevent infinite loops.
207     $_menu['items'] = array();
208
209     $cid = "menu:$user->uid:$locale";
210     if ($cached = cache_get($cid)) {
211       $_menu = unserialize($cached->data);
212     }
213     else {
214       _menu_build();
215       // Cache the menu structure for this user, to expire after one day.
216       cache_set($cid, serialize($_menu), time() + (60 * 60 * 24));
217     }
218
219     // Make sure items that cannot be cached are added.
220     _menu_append_contextual_items();
221
222     // Reset the cached $menu in menu_get_item().
223     menu_get_item(NULL, NULL, TRUE);
224   }
225
226   return $_menu;
227 }
228
229 /**
230  * Return the local task tree.
231  *
232  * Unlike the rest of the menu structure, the local task tree cannot be cached
233  * nor determined too early in the page request, because the user's current
234  * location may be changed by a menu_set_location() call, and the tasks shown
235  * (just as the breadcrumb trail) need to reflect the changed location.
236  */
237 function menu_get_local_tasks() {
238   global $_menu;
239
240   // Don't cache the local task tree, as it varies by location and tasks are
241   // allowed to be dynamically determined.
242   if (!isset($_menu['local tasks'])) {
243     // _menu_build_local_tasks() may indirectly call this function, so prevent
244     // infinite loops.
245     $_menu['local tasks'] = array();
246     $pid = menu_get_active_nontask_item();
247     if (!_menu_build_local_tasks($pid)) {
248       // If the build returned FALSE, the tasks need not be displayed.
249       $_menu['local tasks'][$pid]['children'] = array();
250     }
251   }
252
253   return $_menu['local tasks'];
254 }
255
256 /**
257  * Retrieves the menu item specified by $mid, or by $path if $mid is not given.
258  *
259  * @param $mid
260  *   The menu ID of the menu item to retrieve.
261  * @param $path
262  *   The internal path of the menu item to retrieve. Defaults to NULL. Only
263  *   used if $mid is not set.
264  * @param $reset
265  *   Optional flag that resets the static variable cache of the menu tree, if
266  *   set to TRUE. Default is FALSE.
267  *
268  * @return
269  *   The menu item found in the site menu, or an empty array if none could be
270  *   found.
271  */
272 function menu_get_item($mid, $path = NULL, $reset = FALSE) {
273   static $menu;
274
275   if (!isset($menu) || $reset) {
276     $menu = menu_get_menu();
277   }
278
279   if (isset($mid)) {
280     return $menu['items'][$mid];
281   }
282
283   if (isset($path)) {
284     return $menu['items'][$menu['path index'][$path]];
285   }
286
287   return array();
288 }
289
290 /**
291  * Retrieves the menu ID and title of all root menus.
292  *
293  * @return
294  *   Array containing all menus (but not menu items), in the form mid => title.
295  */
296 function menu_get_root_menus() {
297   $menu = menu_get_menu();
298   $root_menus = array();
299
300   foreach ($menu['items'][0]['children'] as $mid) {
301     $root_menus[$mid] = $menu['items'][$mid]['title'];
302   }
303
304   return $root_menus;
305 }
306
307 /**
308  * Change the current menu location of the user.
309  *
310  * Frequently, modules may want to make a page or node act as if it were
311  * in the menu tree somewhere, even though it was not registered in a
312  * hook_menu() implementation. If the administrator has rearranged the menu,
313  * the newly set location should respect this in the breadcrumb trail and
314  * expanded/collapsed status of menu items in the tree. This function
315  * allows this behavior.
316  *
317  * @param $location
318  *   An array specifying a complete or partial breadcrumb trail for the
319  *   new location, in the same format as the return value of hook_menu().
320  *   The last element of this array should be the new location itself.
321  *
322  * This function will set the new breadcrumb trail to the passed-in value,
323  * but if any elements of this trail are visible in the site tree, the
324  * trail will be "spliced in" to the existing site navigation at that point.
325  */
326 function menu_set_location($location) {
327   global $_menu;
328   $temp_id = min(array_keys($_menu['items'])) - 1;
329   $prev_id = 0;
330
331   // Don't allow this function to change the actual current path, just the
332   // position in the menu tree.
333   $location[count($location) - 1]['path'] = $_GET['q'];
334
335   foreach (array_reverse($location) as $item) {
336     if (isset($_menu['path index'][$item['path']])) {
337       $mid = $_menu['path index'][$item['path']];
338       if (isset($_menu['visible'][$mid])) {
339         // Splice in the breadcrumb at this location.
340         if ($prev_id) {
341           $_menu['items'][$prev_id]['pid'] = $mid;
342         }
343         $prev_id = 0;
344         break;
345       }
346       else {
347         // A hidden item; show it, but only temporarily.
348         $_menu['items'][$mid]['type'] |= MENU_VISIBLE_IN_BREADCRUMB;
349         if ($prev_id) {
350           $_menu['items'][$prev_id]['pid'] = $mid;
351         }
352         $prev_id = $mid;
353       }
354     }
355     else {
356       $item['type'] |= MENU_VISIBLE_IN_BREADCRUMB;
357       if ($prev_id) {
358         $_menu['items'][$prev_id]['pid'] = $temp_id;
359       }
360       $_menu['items'][$temp_id] = $item;
361       $_menu['path index'][$item['path']] = $temp_id;
362
363       $prev_id = $temp_id;
364       $temp_id--;
365     }
366   }
367
368   if ($prev_id) {
369     // Didn't find a home, so attach this to the main navigation menu.
370     $_menu['items'][$prev_id]['pid'] = 1;
371   }
372
373   $final_item = array_pop($location);
374   menu_set_active_item($final_item['path']);
375 }
376
377 /**
378  * Execute the handler associated with the active menu item.
379  *
380  * This is called early in the page request. The active menu item is at
381  * this point determined exclusively by the URL. The handler that is called
382  * here may, as a side effect, change the active menu item so that later
383  * menu functions (that display the menus and breadcrumbs, for example)
384  * act as if the user were in a different location on the site.
385  */
386 function menu_execute_active_handler() {
387   if (_menu_site_is_offline()) {
388     return MENU_SITE_OFFLINE;
389   }
390
391   $menu = menu_get_menu();
392
393   // Determine the menu item containing the callback.
394   $path = $_GET['q'];
395   while ($path && !isset($menu['callbacks'][$path])) {
396     $path = substr($path, 0, strrpos($path, '/'));
397   }
398
399   if (!isset($menu['callbacks'][$path])) {
400     return MENU_NOT_FOUND;
401   }
402
403   if (!function_exists($menu['callbacks'][$path]['callback'])) {
404     return MENU_NOT_FOUND;
405   }
406
407   if (!_menu_item_is_accessible(menu_get_active_item())) {
408     return MENU_ACCESS_DENIED;
409   }
410
411   // We found one, and are allowed to execute it.
412   $arguments = isset($menu['callbacks'][$path]['callback arguments']) ? $menu['callbacks'][$path]['callback arguments'] : array();
413   $arg = substr($_GET['q'], strlen($path) + 1);
414   if (strlen($arg)) {
415     $arguments = array_merge($arguments, explode('/', $arg));
416   }
417
418   return call_user_func_array($menu['callbacks'][$path]['callback'], $arguments);
419 }
420
421 /**
422  * Returns the ID of the active menu item.
423  */
424 function menu_get_active_item() {
425   return menu_set_active_item();
426 }
427
428 /**
429  * Sets the path of the active menu item.
430  */
431 function menu_set_active_item($path = NULL) {
432   static $stored_mid;
433
434   if (!isset($stored_mid) || isset($path)) {
435     $menu = menu_get_menu();
436     if (!isset($path)) {
437       $path = $_GET['q'];
438     }
439     else {
440       $_GET['q'] = $path;
441     }
442
443     while ($path && !isset($menu['path index'][$path])) {
444       $path = substr($path, 0, strrpos($path, '/'));
445     }
446     $stored_mid = isset($menu['path index'][$path]) ? $menu['path index'][$path] : 0;
447
448     // Search for default local tasks to activate instead of this item.
449     $continue = TRUE;
450     while ($continue) {
451       $continue = FALSE;
452       if (isset($menu['items'][$stored_mid]['children'])) {
453         foreach ($menu['items'][$stored_mid]['children'] as $cid) {
454           if ($menu['items'][$cid]['type'] & MENU_LINKS_TO_PARENT) {
455             $stored_mid = $cid;
456             $continue = TRUE;
457           }
458         }
459       }
460     }
461
462     // Reset the cached $menu in menu_get_item().
463     menu_get_item(NULL, NULL, TRUE);
464   }
465
466   return $stored_mid;
467 }
468
469 /**
470  * Returns the ID of the current menu item or, if the current item is a
471  * local task, the menu item to which this task is attached.
472  */
473 function menu_get_active_nontask_item() {
474   $mid = menu_get_active_item();
475
476   // Find the first non-task item:
477   while ($mid) {
478     $item = menu_get_item($mid);
479
480     if (!($item['type'] & MENU_IS_LOCAL_TASK)) {
481       return $mid;
482     }
483
484     $mid = $item['pid'];
485   }
486 }
487
488 /**
489  * Returns the title of the active menu item.
490  */
491 function menu_get_active_title() {
492   if ($mid = menu_get_active_nontask_item()) {
493     $item = menu_get_item($mid);
494     return $item['title'];
495   }
496 }
497
498 /**
499  * Returns the help associated with the active menu item.
500  */
501 function menu_get_active_help() {
502   $path = $_GET['q'];
503   $output = '';
504
505   if (!_menu_item_is_accessible(menu_get_active_item())) {
506     // Don't return help text for areas the user cannot access.
507     return;
508   }
509
510   foreach (module_list() as $name) {
511     if (module_hook($name, 'help')) {
512       if ($temp = module_invoke($name, 'help', $path)) {
513         $output .= $temp . "\n";
514       }
515       if (module_hook('help', 'page')) {
516         if (substr($path, 0, 6) == "admin/") {
517           if (module_invoke($name, 'help', 'admin/help#' . substr($path, 6))) {
518             $output .= theme("more_help_link", url('admin/help/' . substr($path, 6)));
519           }
520         }
521       }
522     }
523   }
524   return $output;
525 }
526
527 /**
528  * Returns an array of rendered menu items in the active breadcrumb trail.
529  */
530 function menu_get_active_breadcrumb() {
531
532   // No breadcrumb for the front page.
533   if (drupal_is_front_page()) {
534     return array();
535   }
536
537   $links[] = l(t('Home'), variable_get('site_frontpage', 'node'));
538
539   $trail = _menu_get_active_trail();
540   foreach ($trail as $mid) {
541     $item = menu_get_item($mid);
542     if ($item['type'] & MENU_VISIBLE_IN_BREADCRUMB) {
543       $links[] = menu_item_link($mid);
544     }
545   }
546
547   // The last item in the trail is the page title; don't display it here.
548   array_pop($links);
549
550   return $links;
551 }
552
553 /**
554  * Returns true when the menu item is in the active trail.
555  */
556 function menu_in_active_trail($mid) {
557   $trail = _menu_get_active_trail();
558
559   return in_array($mid, $trail);
560 }
561
562 /**
563  * Returns true when the menu item is in the active trail within a
564  * specific subsection of the menu tree.
565  *
566  * @param $mid
567  *   The menu item being considered.
568  * @param $pid
569  *   The root of the subsection of the menu tree in which to look.
570  */
571 function menu_in_active_trail_in_submenu($mid, $pid) {
572   $trail = _menu_get_active_trail_in_submenu($pid);
573
574   if (!$trail) {
575     return FALSE;
576   }
577
578   return in_array($mid, $trail);
579 }
580
581 /**
582  * Populate the database representation of the menu.
583  *
584  * This need only be called at the start of pages that modify the menu.
585  */
586 function menu_rebuild() {
587   // Clear the page cache, so that changed menus are reflected for anonymous users.
588   cache_clear_all();
589   // Also clear the menu cache.
590   cache_clear_all('menu:', TRUE);
591
592   _menu_build();
593
594   if (module_exist('menu')) {
595     $menu = menu_get_menu();
596
597     // Fill a queue of new menu items which are modifiable.
598     $new_items = array();
599     foreach ($menu['items'] as $mid => $item) {
600       if ($mid < 0 && ($item['type'] & MENU_MODIFIABLE_BY_ADMIN)) {
601         $new_items[$mid] = $item;
602       }
603     }
604
605     $old_count = -1;
606     // Save the new items updating the pids in each iteration
607     while (($c = count($new_items)) && ($c != $old_count)) {
608       $old_count = count($new_items);
609       foreach($new_items as $mid => $item) {
610         // If the item has a valid parent, save it
611         if ($item['pid'] >= 0) {
612           // The new menu ID gets passed back by reference as $item['mid']
613           menu_save_item($item);
614           // Fix parent IDs for the children of the menu item just saved
615           if ($item['children']) {
616             foreach ($item['children'] as $child) {
617               if (isset($new_items[$child])) {
618                 $new_items[$child]['pid'] = $item['mid'];
619               }
620             }
621           }
622           // remove the item
623           unset($new_items[$mid]);
624         }
625       }
626     }
627     // Rebuild the menu to account for the changes.
628     _menu_build();
629   }
630
631   // Reset the cached $menu in menu_get_item().
632   menu_get_item(NULL, NULL, TRUE);
633
634 }
635
636 /**
637  * Generate the HTML for a menu tree.
638  *
639  * @param $pid
640  *   The parent id of the menu.
641  *
642  * @ingroup themeable
643  */
644 function theme_menu_tree($pid = 1) {
645   if ($tree = menu_tree($pid)) {
646     return "\n<ul class=\"menu\">\n". $tree ."\n</ul>\n";
647   }
648 }
649
650 /**
651  * Returns a rendered menu tree.
652  *
653  * @param $pid
654  *   The parent id of the menu.
655  */
656 function menu_tree($pid = 1) {
657   $menu = menu_get_menu();
658   $output = '';
659
660   if (isset($menu['visible'][$pid]) && $menu['visible'][$pid]['children']) {
661     foreach ($menu['visible'][$pid]['children'] as $mid) {
662       $type = isset($menu['visible'][$mid]['type']) ? $menu['visible'][$mid]['type'] : NULL;
663       $children = isset($menu['visible'][$mid]['children']) ? $menu['visible'][$mid]['children'] : NULL;
664       $output .= theme('menu_item', $mid, menu_in_active_trail($mid) || ($type & MENU_EXPANDED) ? theme('menu_tree', $mid) : '', count($children) == 0);
665     }
666   }
667
668   return $output;
669 }
670
671 /**
672  * Generate the HTML output for a single menu item.
673  *
674  * @param $mid
675  *   The menu id of the item.
676  * @param $children
677  *   A string containing any rendered child items of this menu.
678  * @param $leaf
679  *   A boolean indicating whether this menu item is a leaf.
680  *
681  * @ingroup themeable
682  */
683 function theme_menu_item($mid, $children = '', $leaf = TRUE) {
684   return '<li class="'. ($leaf ? 'leaf' : ($children ? 'expanded' : 'collapsed')) .'">'. menu_item_link($mid) . $children ."</li>\n";
685 }
686
687 /**
688  * Generate the HTML representing a given menu item ID.
689  *
690  * @param $item
691  *   The menu item to render.
692  * @param $link_item
693  *   The menu item which should be used to find the correct path.
694  *
695  * @ingroup themeable
696  */
697 function theme_menu_item_link($item, $link_item) {
698   return l($item['title'], $link_item['path'], isset($item['description']) ? array('title' => $item['description']) : array());
699 }
700
701 /**
702  * Returns the rendered link to a menu item.
703  *
704  * @param $mid
705  *   The menu item id to render.
706  */
707 function menu_item_link($mid) {
708   $item = menu_get_item($mid);
709   $link_item = $item;
710
711   while ($link_item['type'] & MENU_LINKS_TO_PARENT) {
712     $link_item = menu_get_item($link_item['pid']);
713   }
714
715   return theme('menu_item_link', $item, $link_item);
716 }
717
718 /**
719  * Returns the rendered local tasks. The default implementation renders
720  * them as tabs.
721  *
722  * @ingroup themeable
723  */
724 function theme_menu_local_tasks() {
725   $output = '';
726
727   if ($primary = menu_primary_local_tasks()) {
728     $output .= "<ul class=\"tabs primary\">\n". $primary ."</ul>\n";
729   }
730   if ($secondary = menu_secondary_local_tasks()) {
731     $output .= "<ul class=\"tabs secondary\">\n". $secondary ."</ul>\n";
732   }
733
734   return $output;
735 }
736
737 /**
738  * Returns the rendered HTML of the primary local tasks.
739  */
740 function menu_primary_local_tasks() {
741   $local_tasks = menu_get_local_tasks();
742   $pid = menu_get_active_nontask_item();
743   $output = '';
744
745   if (count($local_tasks[$pid]['children'])) {
746     foreach ($local_tasks[$pid]['children'] as $mid) {
747       $output .= theme('menu_local_task', $mid, menu_in_active_trail($mid), TRUE);
748     }
749   }
750
751   return $output;
752 }
753
754 /**
755  * Returns the rendered HTML of the secondary local tasks.
756  */
757 function menu_secondary_local_tasks() {
758   $local_tasks = menu_get_local_tasks();
759   $pid = menu_get_active_nontask_item();
760   $output = '';
761
762   if (count($local_tasks[$pid]['children'])) {
763     foreach ($local_tasks[$pid]['children'] as $mid) {
764       if (menu_in_active_trail($mid) && count($local_tasks[$mid]['children']) > 1) {
765         foreach ($local_tasks[$mid]['children'] as $cid) {
766           $output .= theme('menu_local_task', $cid, menu_in_active_trail($cid), FALSE);
767         }
768       }
769     }
770   }
771
772   return $output;
773 }
774
775 /**
776  * Generate the HTML representing a given menu item ID as a tab.
777  *
778  * @param $mid
779  *   The menu ID to render.
780  * @param $active
781  *   Whether this tab or a subtab is the active menu item.
782  * @param $primary
783  *   Whether this tab is a primary tab or a subtab.
784  *
785  * @ingroup themeable
786  */
787 function theme_menu_local_task($mid, $active, $primary) {
788   if ($active) {
789     return '<li class="active">'. menu_item_link($mid) ."</li>\n";
790   }
791   else {
792     return '<li>'. menu_item_link($mid) ."</li>\n";
793   }
794 }
795
796 /**
797  * Returns an array containing the primary links.
798  * Can optionally descend from the root of the Primary links menu towards the
799  * current node for a specified number of levels and return that submenu.
800  * Used to generate a primary/secondary menu from different levels of one menu.
801  *
802  * @param $start_level
803  *   This optional parameter can be used to retrieve a context-sensitive array
804  *   of links at $start_level levels deep into the Primary links menu.
805  *   The default is to return the top-level links.
806  * @param $pid
807  *   The parent menu ID from which to search for children. Defaults to the
808  *   menu_primary_menu setting.
809  * @return An array containing the themed links as the values. The keys of
810  *   the array contain some extra encoded information about the results.
811  *   The format of the key is {level}-{num}{-active}.
812  *   level is the depth within the menu tree of this list.
813  *   num is the number within this array, used only to make the key unique.
814  *   -active is appended if this element is in the active trail.
815  */
816 function menu_primary_links($start_level = 1, $pid = 0) {
817   if (!module_exist('menu')) {
818     return NULL;
819   }
820   if (!$pid) {
821     $pid = variable_get('menu_primary_menu', 0);
822   }
823   if (!$pid) {
824     return NULL;
825   }
826
827   if ($start_level < 1) {
828     $start_level = 1;
829   }
830
831   if ($start_level > 1) {
832     $trail = _menu_get_active_trail_in_submenu($pid);
833     if (!$trail) {
834       return NULL;
835     }
836     else {
837       $pid = $trail[$start_level - 1];
838     }
839   }
840
841   $menu = menu_get_menu();
842   $links = array();
843   if ($pid && is_array($menu['visible'][$pid]) && isset($menu['visible'][$pid]['children'])) {
844     $count = 1;
845     foreach ($menu['visible'][$pid]['children'] as $cid) {
846       $index = "menu-$start_level-$count";
847       if (menu_in_active_trail_in_submenu($cid, $pid)) {
848         $index .= "-active";
849       }
850       $links[$index] = menu_item_link($cid);
851       $count++;
852     }
853   }
854
855   // Special case - provide link to admin/menu if primary links is empty.
856   if (empty($links) && $start_level == 1 && $pid == variable_get('menu_primary_menu', 0)) {
857     $links['1-1'] = l(t('edit primary links'),'admin/menu');
858   }
859
860   return $links;
861 }
862
863 /**
864  * Returns an array containing the secondary links.
865  * Secondary links can be either a second level of the Primary links
866  * menu or generated from their own menu.
867  */
868 function menu_secondary_links() {
869   $msm = variable_get('menu_secondary_menu', 0);
870   if ($msm == 0) {
871     return NULL;
872   }
873
874   if ($msm == variable_get('menu_primary_menu', 0)) {
875     return menu_primary_links(2, $msm);
876   }
877
878   return menu_primary_links(1, $msm);
879 }
880
881 /**
882  * Returns the themed HTML for primary and secondary links.
883  * Note that this function is overridden by most core themes because
884  * those themes display links in "link | link" format, not from a list.
885  * Also note that by default links rendered with this function are
886  * displayed with the same CSS as is used for the local tasks.
887  * If a theme wishes to render links from a ul it is expected that
888  * the theme will provide suitable CSS.
889  *
890  * @param $links
891  *   An array containing links to render.
892  * @return
893  *   A string containing the themed links.
894  *
895  * @ingroup themeable
896  */
897 function theme_menu_links($links) {
898   if (!count($links)) {
899     return '';
900   }
901   $level_tmp = explode('-', key($links));
902   $level = $level_tmp[0];
903   $output = "<ul class=\"links-$level\">\n";
904   foreach ($links as $index => $link) {
905     $output .= '<li';
906     if (stristr($index, 'active')) {
907       $output .= ' class="active"';
908     }
909     $output .= ">$link</li>\n";
910   }
911   $output .= '</ul>';
912
913   return $output;
914 }
915
916 /**
917  * @} End of "defgroup menu".
918  */
919
920 /**
921  * Returns an array with the menu items that lead to the current menu item.
922  */
923 function _menu_get_active_trail() {
924   static $trail;
925
926   if (!isset($trail)) {
927     $trail = array();
928
929     $mid = menu_get_active_item();
930
931     // Follow the parents up the chain to get the trail.
932     while ($mid && ($item = menu_get_item($mid))) {
933       array_unshift($trail, $mid);
934       $mid = $item['pid'];
935     }
936   }
937   return $trail;
938 }
939
940 /**
941  * Find the active trail through a specific subsection of the menu tree.
942  *
943  * @param $pid
944  *   The root item from which the active trail must descend.
945  */
946 function _menu_get_active_trail_in_submenu($pid) {
947   static $trails;
948
949   if (!isset($trails)) {
950     // Find all menu items which point to the current node and for each
951     // follow the parents up the chain to build an active trail.
952     $trails = array();
953     $menu = menu_get_menu();
954     $path = $_GET['q'];
955     $count = 0;
956     while ($path && !$count) {
957       foreach ($menu['items'] as $key => $item) {
958         if (isset($item['path']) && $item['path'] == $path) {
959           $trails[$count] = array();
960           $mid = $key;
961           while ($mid && $menu['items'][$mid]) {
962             array_unshift($trails[$count], $mid);
963             $mid = $menu['items'][$mid]['pid'];
964           }
965           $count ++;
966         }
967       }
968       $path = substr($path, 0, strrpos($path, '/'));
969     }
970   }
971
972   if ($trails) {
973     foreach ($trails as $trail) {
974       $count_trail = count($trail);
975       for ($i = 0; $i < $count_trail; $i++) {
976         if ($trail[$i] == $pid) {
977           // Return a trail from $pid down to the current page inclusive.
978           for ( ; $i < $count_trail; $i++) {
979             $subtrail[] = $trail[$i];
980           }
981           return $subtrail;
982         }
983       }
984     }
985   }
986
987   return NULL;
988 }
989
990 /**
991  * Comparator routine for use in sorting menu items.
992  */
993 function _menu_sort($a, $b) {
994   $menu = menu_get_menu();
995
996   $a = &$menu['items'][$a];
997   $b = &$menu['items'][$b];
998
999   if ($a['weight'] < $b['weight']) {
1000     return -1;
1001   }
1002   elseif ($a['weight'] > $b['weight']) {
1003     return 1;
1004   }
1005   elseif (isset($a['title']) && isset($b['title'])) {
1006     return strnatcasecmp($a['title'], $b['title']);
1007   }
1008   else {
1009     return 1;
1010   }
1011 }
1012
1013 /**
1014  * Build the menu by querying both modules and the database.
1015  */
1016 function _menu_build() {
1017   global $_menu;
1018   global $user;
1019
1020   // Start from a clean slate.
1021   $_menu = array();
1022
1023   $_menu['path index'] = array();
1024   // Set up items array, including default "Navigation" menu.
1025   $_menu['items'] = array(
1026     0 => array('path' => '', 'title' => '', 'type' => MENU_IS_ROOT),
1027     1 => array('pid' => 0, 'path' => '', 'title' => t('Navigation'), 'weight' => -50, 'access' => TRUE, 'type' => MENU_IS_ROOT | MENU_VISIBLE_IN_TREE)
1028     );
1029   $_menu['callbacks'] = array();
1030
1031   // Build a sequential list of all menu items.
1032   $menu_item_list = module_invoke_all('menu', TRUE);
1033
1034   // Menu items not in the DB get temporary negative IDs.
1035   $temp_mid = -1;
1036
1037   foreach ($menu_item_list as $item) {
1038     if (!isset($item['path'])) {
1039       $item['path'] = '';
1040     }
1041     if (!isset($item['type'])) {
1042       $item['type'] = MENU_NORMAL_ITEM;
1043     }
1044     if (!isset($item['weight'])) {
1045       $item['weight'] = 0;
1046     }
1047     $mid = $temp_mid;
1048     if (isset($_menu['path index'][$item['path']])) {
1049       // Newer menu items overwrite older ones.
1050       unset($_menu['items'][$_menu['path index'][$item['path']]]);
1051     }
1052     if (isset($item['callback'])) {
1053       $_menu['callbacks'][$item['path']] = array('callback' => $item['callback']);
1054       if (isset($item['callback arguments'])) {
1055         $_menu['callbacks'][$item['path']]['callback arguments'] = $item['callback arguments'];
1056       }
1057     }
1058     unset($item['callback']);
1059     unset($item['callback arguments']);
1060     $_menu['items'][$mid] = $item;
1061     $_menu['path index'][$item['path']] = $mid;
1062
1063     $temp_mid--;
1064   }
1065
1066   // Now fetch items from the DB, reassigning menu IDs as needed.
1067   if (module_exist('menu')) {
1068     $result = db_query(db_rewrite_sql('SELECT m.mid, m.* FROM {menu} m ORDER BY m.mid ASC', 'm', 'mid'));
1069     while ($item = db_fetch_object($result)) {
1070       // Handle URL aliases if entered in menu administration.
1071       if (!isset($_menu['path index'][$item->path])) {
1072         $item->path = drupal_get_normal_path($item->path);
1073       }
1074       if (isset($_menu['path index'][$item->path])) {
1075         // The path is already declared.
1076         $old_mid = $_menu['path index'][$item->path];
1077         if ($old_mid < 0) {
1078           // It had a temporary ID, so use a permanent one.
1079           $_menu['items'][$item->mid] = $_menu['items'][$old_mid];
1080           unset($_menu['items'][$old_mid]);
1081           $_menu['path index'][$item->path] = $item->mid;
1082         }
1083         else {
1084           // It has a permanent ID. Only replace with non-custom menu items.
1085           if ($item->type & MENU_CREATED_BY_ADMIN) {
1086             $_menu['items'][$item->mid] = array('path' => $item->path);
1087           }
1088           else {
1089             // Leave the old item around as a shortcut to this one.
1090             $_menu['items'][$item->mid] = $_menu['items'][$old_mid];
1091             $_menu['path index'][$item->path] = $item->mid;
1092           }
1093         }
1094       }
1095       else {
1096         // The path was not declared, so this is a custom item or an orphaned one.
1097         if ($item->type & MENU_CREATED_BY_ADMIN) {
1098           $_menu['items'][$item->mid] = array('path' => $item->path);
1099           if (!empty($item->path)) {
1100             $_menu['path index'][$item->path] = $item->mid;
1101           }
1102         }
1103       }
1104
1105       // If the administrator has changed the item, reflect the change.
1106       if ($item->type & MENU_MODIFIED_BY_ADMIN) {
1107         $_menu['items'][$item->mid]['title'] = $item->title;
1108         $_menu['items'][$item->mid]['description'] = $item->description;
1109         $_menu['items'][$item->mid]['pid'] = $item->pid;
1110         $_menu['items'][$item->mid]['weight'] = $item->weight;
1111         $_menu['items'][$item->mid]['type'] = $item->type;
1112       }
1113     }
1114   }
1115
1116   // Associate parent and child menu items.
1117   _menu_find_parents($_menu['items']);
1118
1119   // Prepare to display trees to the user as required.
1120   _menu_build_visible_tree();
1121 }
1122
1123 /**
1124  * Determine whether the given menu item is accessible to the current user.
1125  *
1126  * Use this instead of just checking the "access" property of a menu item
1127  * to properly handle items with fall-through semantics.
1128  */
1129 function _menu_item_is_accessible($mid) {
1130   $menu = menu_get_menu();
1131
1132   // Follow the path up to find the first "access" attribute.
1133   $path = isset($menu['items'][$mid]['path']) ? $menu['items'][$mid]['path'] : NULL;
1134   while ($path && (!isset($menu['path index'][$path]) || !isset($menu['items'][$menu['path index'][$path]]['access']))) {
1135     $path = substr($path, 0, strrpos($path, '/'));
1136   }
1137   if (empty($path)) {
1138     // Items without any access attribute up the chain are denied, unless they
1139     // were created by the admin. They most likely point to non-Drupal directories
1140     // or to an external URL and should be allowed.
1141     return $menu['items'][$mid]['type'] & MENU_CREATED_BY_ADMIN;
1142   }
1143   return $menu['items'][$menu['path index'][$path]]['access'];
1144 }
1145
1146 /**
1147  * Find all visible items in the menu tree, for ease in displaying to user.
1148  *
1149  * Since this is only for display, we only need title, path, and children
1150  * for each item.
1151  */
1152 function _menu_build_visible_tree($pid = 0) {
1153   global $_menu;
1154
1155   if (isset($_menu['items'][$pid])) {
1156     $parent = $_menu['items'][$pid];
1157
1158     $children = array();
1159     if (isset($parent['children'])) {
1160       usort($parent['children'], '_menu_sort');
1161       foreach ($parent['children'] as $mid) {
1162         $children = array_merge($children, _menu_build_visible_tree($mid));
1163       }
1164     }
1165     $visible = ($parent['type'] & MENU_VISIBLE_IN_TREE) ||
1166       ($parent['type'] & MENU_VISIBLE_IF_HAS_CHILDREN && count($children) > 0);
1167     $allowed = _menu_item_is_accessible($pid);
1168
1169     if (($parent['type'] & MENU_IS_ROOT) || ($visible && $allowed)) {
1170       $_menu['visible'][$pid] = array('title' => $parent['title'], 'path' => $parent['path'], 'children' => $children, 'type' => $parent['type']);
1171       foreach ($children as $mid) {
1172         $_menu['visible'][$mid]['pid'] = $pid;
1173       }
1174       return array($pid);
1175     }
1176     else {
1177       return $children;
1178     }
1179   }
1180
1181   return array();
1182 }
1183
1184 /**
1185  * Account for menu items that are only defined at certain paths, so will not
1186  * be cached.
1187  *
1188  * We don't support the full range of menu item options for these menu items. We
1189  * don't support MENU_VISIBLE_IF_HAS_CHILDREN, and we require parent items to be
1190  * declared before their children.
1191  */
1192 function _menu_append_contextual_items() {
1193   global $_menu;
1194
1195   // Build a sequential list of all menu items.
1196   $menu_item_list = module_invoke_all('menu', FALSE);
1197
1198   // Menu items not in the DB get temporary negative IDs.
1199   $temp_mid = min(array_keys($_menu['items'])) - 1;
1200   $new_items = array();
1201
1202   foreach ($menu_item_list as $item) {
1203     if (isset($item['callback'])) {
1204       $_menu['callbacks'][$item['path']] = array('callback' => $item['callback']);
1205       if (isset($item['callback arguments'])) {
1206         $_menu['callbacks'][$item['path']]['callback arguments'] = $item['callback arguments'];
1207       }
1208     }
1209     unset($item['callback']);
1210     unset($item['callback arguments']);
1211     if (!isset($_menu['path index'][$item['path']])) {
1212       if (!isset($item['path'])) {
1213         $item['path'] = '';
1214       }
1215       if (!isset($item['type'])) {
1216         $item['type'] = MENU_NORMAL_ITEM;
1217       }
1218       if (!isset($item['weight'])) {
1219         $item['weight'] = 0;
1220       }
1221       $_menu['items'][$temp_mid] = $item;
1222       $_menu['path index'][$item['path']] = $temp_mid;
1223       $new_items[$temp_mid] = $item;
1224       $temp_mid--;
1225     }
1226     else {
1227       $mid = $_menu['path index'][$item['path']];
1228       if ($_menu['items'][$mid]['type'] & MENU_CREATED_BY_ADMIN) {
1229         $_menu['items'][$mid]['access'] = $item['access'];
1230         if (isset($_menu['items'][$mid]['callback'])) {
1231           $_menu['items'][$mid]['callback'] = $item['callback'];
1232         }
1233         if (isset($_menu['items'][$mid]['callback arguments'])) {
1234           $_menu['items'][$mid]['callback arguments'] = $item['callback arguments'];
1235         }
1236       }
1237     }
1238   }
1239
1240   // Establish parent-child relationships.
1241   _menu_find_parents($new_items);
1242
1243   // Add new items to the visible tree if necessary.
1244   foreach ($new_items as $mid => $item) {
1245     $item = $_menu['items'][$mid];
1246     if (($item['type'] & MENU_VISIBLE_IN_TREE) && _menu_item_is_accessible($mid)) {
1247       $pid = $item['pid'];
1248       while ($pid && !isset($_menu['visible'][$pid])) {
1249         $pid = $_menu['items'][$pid]['pid'];
1250       }
1251       $_menu['visible'][$mid] = array('title' => $item['title'], 'path' => $item['path'], 'pid' => $pid);
1252       $_menu['visible'][$pid]['children'][] = $mid;
1253       usort($_menu['visible'][$pid]['children'], '_menu_sort');
1254     }
1255   }
1256 }
1257
1258 /**
1259  * Establish parent-child relationships.
1260  */
1261 function _menu_find_parents(&$items) {
1262   global $_menu;
1263
1264   foreach ($items as $mid => $item) {
1265     if (!isset($item['pid'])) {
1266       // Parent's location has not been customized, so figure it out using the path.
1267       $parent = $item['path'];
1268       if ($parent) {
1269         do {
1270           $parent = substr($parent, 0, strrpos($parent, '/'));
1271         }
1272         while ($parent && !isset($_menu['path index'][$parent]));
1273       }
1274
1275       $pid = $parent ? $_menu['path index'][$parent] : 1;
1276       $_menu['items'][$mid]['pid'] = $pid;
1277     }
1278     else {
1279       $pid = $item['pid'];
1280     }
1281
1282     // Don't make root a child of itself.
1283     if ($mid) {
1284       if (isset ($_menu['items'][$pid])) {
1285         $_menu['items'][$pid]['children'][] = $mid;
1286       }
1287       else {
1288         // If parent is missing, it is a menu item that used to be defined
1289         // but is no longer. Default to a root-level "Navigation" menu item.
1290         $_menu['items'][1]['children'][] = $mid;
1291       }
1292     }
1293   }
1294 }
1295
1296 /**
1297  * Find all the items in the current local task tree.
1298  *
1299  * Since this is only for display, we only need title, path, and children
1300  * for each item.
1301  *
1302  * At the close of this function, $_menu['local tasks'] is populated with the
1303  * menu items in the local task tree.
1304  *
1305  * @return
1306  *   TRUE if the local task tree is forked. It does not need to be displayed
1307  *   otherwise.
1308  */
1309 function _menu_build_local_tasks($pid) {
1310   global $_menu;
1311
1312   $forked = FALSE;
1313
1314   if (isset($_menu['items'][$pid])) {
1315     $parent = $_menu['items'][$pid];
1316
1317     $children = array();
1318     if (isset($parent['children'])) {
1319       foreach ($parent['children'] as $mid) {
1320         if (($_menu['items'][$mid]['type'] & MENU_IS_LOCAL_TASK) && _menu_item_is_accessible($mid)) {
1321           $children[] = $mid;
1322           // Beware short-circuiting || operator!
1323           $forked = _menu_build_local_tasks($mid) || $forked;
1324         }
1325       }
1326     }
1327     usort($children, '_menu_sort');
1328     $forked = $forked || count($children) > 1;
1329
1330     $_menu['local tasks'][$pid] = array('title' => $parent['title'], 'path' => $parent['path'], 'children' => $children);
1331     foreach ($children as $mid) {
1332       $_menu['local tasks'][$mid]['pid'] = $pid;
1333     }
1334   }
1335
1336   return $forked;
1337 }
1338
1339 /**
1340  * Returns TRUE if the site is off-line for maintenance.
1341  */
1342 function _menu_site_is_offline() {
1343   // Check if site is set to off-line mode
1344   if (variable_get('site_offline', 0)) {
1345     // Check if the user has administration privileges
1346     if (!user_access('administer site configuration')) {
1347       // Check if this is an attempt to login
1348       if (drupal_get_normal_path($_GET['q']) != 'user') {
1349         return TRUE;
1350       }
1351     }
1352     else {
1353       $offline_message = t('Operating in off-line mode.');
1354       $messages = drupal_set_message();
1355       // Ensure that the off-line message is displayed only once [allowing for page redirects].
1356       if (!isset($messages) || !isset($messages['status']) || !in_array($offline_message, $messages['status'])) {
1357         drupal_set_message($offline_message);
1358       }
1359     }
1360   }
1361   return FALSE;
1362 }