source: sipes/cord/includes/menu.inc

stableversion-3.0
Last change on this file was 6627152, checked in by José Gregorio Puentes <jpuentes@…>, 8 años ago

se actualizo el cord

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