menu.inc 70.7 KB
Newer Older
1
<?php
2
// $Id$
3

4 5 6 7 8
/**
 * @file
 * API for the Drupal menu system.
 */

Dries's avatar
Dries committed
9 10 11
/**
 * @defgroup menu Menu system
 * @{
12
 * Define the navigation menus, and route page requests to code based on URLs.
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
 *
 * The Drupal menu system drives both the navigation system from a user
 * perspective and the callback system that Drupal uses to respond to URLs
 * passed from the browser. For this reason, a good understanding of the
 * menu system is fundamental to the creation of complex modules.
 *
 * Drupal's menu system follows a simple hierarchy defined by paths.
 * Implementations of hook_menu() define menu items and assign them to
 * paths (which should be unique). The menu system aggregates these items
 * and determines the menu hierarchy from the paths. For example, if the
 * paths defined were a, a/b, e, a/b/c/d, f/g, and a/b/h, the menu system
 * would form the structure:
 * - a
 *   - a/b
 *     - a/b/c/d
 *     - a/b/h
 * - e
 * - f/g
 * Note that the number of elements in the path does not necessarily
 * determine the depth of the menu item in the tree.
 *
 * When responding to a page request, the menu system looks to see if the
 * path requested by the browser is registered as a menu item with a
 * callback. If not, the system searches up the menu tree for the most
 * complete match with a callback it can find. If the path a/b/i is
 * requested in the tree above, the callback for a/b would be used.
 *
40
 * The found callback function is called with any arguments specified
41
 * in the "page arguments" attribute of its menu item. The
42 43 44 45
 * attribute must be an array. After these arguments, any remaining
 * components of the path are appended as further arguments. In this
 * way, the callback for a/b above could respond to a request for
 * a/b/i differently than a request for a/b/j.
46 47 48 49
 *
 * For an illustration of this process, see page_example.module.
 *
 * Access to the callback functions is also protected by the menu system.
50 51 52 53
 * The "access callback" with an optional "access arguments" of each menu
 * item is called before the page callback proceeds. If this returns TRUE,
 * then access is granted; if FALSE, then access is denied. Menu items may
 * omit this attribute to use the value provided by an ancestor item.
54 55 56 57 58 59 60 61 62 63 64 65 66
 *
 * In the default Drupal interface, you will notice many links rendered as
 * tabs. These are known in the menu system as "local tasks", and they are
 * rendered as tabs by default, though other presentations are possible.
 * Local tasks function just as other menu items in most respects. It is
 * convention that the names of these tasks should be short verbs if
 * possible. In addition, a "default" local task should be provided for
 * each set. When visiting a local task's parent menu item, the default
 * local task will be rendered as if it is selected; this provides for a
 * normal tab user experience. This default task is special in that it
 * links not to its provided path, but to its parent item's path instead.
 * The default task's path is only used to place it appropriately in the
 * menu hierarchy.
67 68 69
 *
 * Everything described so far is stored in the menu_router table. The
 * 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().
Dries's avatar
Dries committed
72 73
 */

Dries's avatar
Dries committed
74
/**
75
 * @name Menu flags
76
 * @{
Dries's avatar
Dries committed
77 78
 * Flags for use in the "type" attribute of menu items.
 */
79

Dries's avatar
Dries committed
80 81 82
define('MENU_IS_ROOT', 0x0001);
define('MENU_VISIBLE_IN_TREE', 0x0002);
define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004);
83
define('MENU_LINKS_TO_PARENT', 0x0008);
Dries's avatar
Dries committed
84
define('MENU_MODIFIABLE_BY_ADMIN', 0x0010);
85 86 87
define('MENU_MODIFIED_BY_ADMIN', 0x0020);
define('MENU_CREATED_BY_ADMIN', 0x0040);
define('MENU_IS_LOCAL_TASK', 0x0080);
88

89
/**
90
 * @} End of "Menu flags".
91 92 93
 */

/**
94
 * @name Menu item types
95 96 97 98
 * @{
 * Menu item definitions provide one of these constants, which are shortcuts for
 * combinations of the above flags.
 */
Dries's avatar
Dries committed
99

Dries's avatar
Dries committed
100 101
/**
 * Normal menu items show up in the menu tree and can be moved/hidden by
102 103
 * the administrator. Use this for most menu items. It is the default value if
 * no menu item type is specified.
Dries's avatar
Dries committed
104 105
 */
define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB | MENU_MODIFIABLE_BY_ADMIN);
106

Dries's avatar
Dries committed
107 108
/**
 * Callbacks simply register a path so that the correct function is fired
109
 * when the URL is accessed. They are not shown in the menu.
Dries's avatar
Dries committed
110 111
 */
define('MENU_CALLBACK', MENU_VISIBLE_IN_BREADCRUMB);
112

Dries's avatar
Dries committed
113
/**
114 115
 * Modules may "suggest" menu items that the administrator may enable. They act
 * just as callbacks do until enabled, at which time they act like normal items.
Dries's avatar
Dries committed
116
 */
117
define('MENU_SUGGESTED_ITEM', MENU_MODIFIABLE_BY_ADMIN | MENU_VISIBLE_IN_BREADCRUMB);
Dries's avatar
Dries committed
118 119

/**
120 121 122
 * Local tasks are rendered as tabs by default. Use this for menu items that
 * describe actions to be performed on their parent item. An example is the path
 * "node/52/edit", which performs the "edit" task on "node/52".
Dries's avatar
Dries committed
123 124 125
 */
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK);

126 127 128 129 130 131
/**
 * Every set of local tasks should provide one "default" task, that links to the
 * same path as its parent when clicked.
 */
define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT);

Dries's avatar
Dries committed
132
/**
133
 * @} End of "Menu item types".
134 135 136 137 138
 */

/**
 * @name Menu status codes
 * @{
Dries's avatar
Dries committed
139 140
 * Status codes for menu callbacks.
 */
141

Dries's avatar
Dries committed
142 143 144
define('MENU_FOUND', 1);
define('MENU_NOT_FOUND', 2);
define('MENU_ACCESS_DENIED', 3);
145
define('MENU_SITE_OFFLINE', 4);
146

147
/**
148
 * @} End of "Menu status codes".
149
 */
150

151
/**
152
 * @Name Menu tree parameters
153
 * @{
154
 * Menu tree
155 156
 */

157 158
 /**
 * The maximum number of path elements for a menu callback
159
 */
160
define('MENU_MAX_PARTS', 7);
161

162 163

/**
164
 * The maximum depth of a menu links tree - matches the number of p columns.
165
 */
166
define('MENU_MAX_DEPTH', 9);
167

168 169

/**
170
 * @} End of "Menu tree parameters".
171 172
 */

173
/**
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
 * Returns the ancestors (and relevant placeholders) for any given path.
 *
 * For example, the ancestors of node/12345/edit are:
 *
 * node/12345/edit
 * node/12345/%
 * node/%/edit
 * node/%/%
 * node/12345
 * node/%
 * node
 *
 * To generate these, we will use binary numbers. Each bit represents a
 * part of the path. If the bit is 1, then it represents the original
 * value while 0 means wildcard. If the path is node/12/edit/foo
 * then the 1011 bitstring represents node/%/edit/foo where % means that
190 191 192
 * any argument matches that part.  We limit ourselves to using binary
 * numbers that correspond the patterns of wildcards of router items that
 * actually exists.  This list of 'masks' is built in menu_rebuild().
193 194 195 196 197 198
 *
 * @param $parts
 *   An array of path parts, for the above example
 *   array('node', '12345', 'edit').
 * @return
 *   An array which contains the ancestors and placeholders. Placeholders
199
 *   simply contain as many '%s' as the ancestors.
200 201
 */
function menu_get_ancestors($parts) {
202
  $number_parts = count($parts);
203 204
  $placeholders = array();
  $ancestors = array();
205 206 207 208 209 210 211 212 213 214 215 216 217
  $length =  $number_parts - 1;
  $end = (1 << $number_parts) - 1;
  $masks = variable_get('menu_masks', array());
  // Only examine patterns that actually exist as router items (the masks).
  foreach ($masks as $i) {
    if ($i > $end) {
      // Only look at masks that are not longer than the path of interest.
      continue;
    }
    elseif ($i < (1 << $length)) {
      // We have exhausted the masks of a given length, so decrease the length.
      --$length;
    }
218 219 220 221 222 223 224 225 226 227 228
    $current = '';
    for ($j = $length; $j >= 0; $j--) {
      if ($i & (1 << $j)) {
        $current .= $parts[$length - $j];
      }
      else {
        $current .= '%';
      }
      if ($j) {
        $current .= '/';
      }
229
    }
230 231
    $placeholders[] = "'%s'";
    $ancestors[] = $current;
232
  }
233
  return array($ancestors, $placeholders);
234 235 236
}

/**
237 238 239 240
 * The menu system uses serialized arrays stored in the database for
 * arguments. However, often these need to change according to the
 * current path. This function unserializes such an array and does the
 * necessary change.
241
 *
242
 * Integer values are mapped according to the $map parameter. For
243 244 245 246 247
 * example, if unserialize($data) is array('view', 1) and $map is
 * array('node', '12345') then 'view' will not be changed because
 * it is not an integer, but 1 will as it is an integer. As $map[1]
 * is '12345', 1 will be replaced with '12345'. So the result will
 * be array('node_load', '12345').
248
 *
249 250 251 252
 * @param @data
 *   A serialized array.
 * @param @map
 *   An array of potential replacements.
253
 * @return
254
 *   The $data array unserialized and mapped.
255
 */
256 257 258 259 260 261 262 263
function menu_unserialize($data, $map) {
  if ($data = unserialize($data)) {
    foreach ($data as $k => $v) {
      if (is_int($v)) {
        $data[$k] = isset($map[$v]) ? $map[$v] : '';
      }
    }
    return $data;
264
  }
265 266
  else {
    return array();
267 268 269 270
  }
}

/**
271
 * Get the menu callback for the a path.
272
 *
273
 * @param $path
274
 *   A path, or NULL for the current path
275
 */
276
function menu_get_item($path = NULL) {
277
  static $router_items;
278 279 280
  if (!isset($path)) {
    $path = $_GET['q'];
  }
281
  if (!isset($router_items[$path])) {
282
    $original_map = arg(NULL, $path);
283
    $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
284
    list($ancestors, $placeholders) = menu_get_ancestors($parts);
285

286 287
    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))) {
      $map = _menu_translate($router_item, $original_map);
288
      if ($map === FALSE) {
289
        $router_items[$path] = FALSE;
290
        return FALSE;
291
      }
292 293 294
      if ($router_item['access']) {
        $router_item['map'] = $map;
        $router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $map), array_slice($map, $router_item['number_parts']));
Dries's avatar
Dries committed
295 296
      }
    }
297
    $router_items[$path] = $router_item;
Dries's avatar
Dries committed
298
  }
299
  return $router_items[$path];
Dries's avatar
Dries committed
300 301 302
}

/**
303
 * Execute the page callback associated with the current path
Dries's avatar
Dries committed
304
 */
305
function menu_execute_active_handler($path = NULL) {
306 307
  if (_menu_site_is_offline()) {
    return MENU_SITE_OFFLINE;
308 309
  }
  if ($router_item = menu_get_item($path)) {
310 311 312
    if ($router_item['access']) {
      if ($router_item['file']) {
        require_once($router_item['file']);
313
      }
314
      return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
315 316 317 318
    }
    else {
      return MENU_ACCESS_DENIED;
    }
319
  }
320 321
  return MENU_NOT_FOUND;
}
322

323
/**
324
 * Loads objects into the map as defined in the $item['load_functions'].
325
 *
326
 * @param $item
327
 *   A menu router or menu link item
328 329 330
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
331 332 333
 *   Returns TRUE for success, FALSE if an object cannot be loaded
 */
function _menu_load_objects($item, &$map) {
334 335
  if ($item['load_functions']) {
    $load_functions = unserialize($item['load_functions']);
336 337 338
    $path_map = $map;
    foreach ($load_functions as $index => $function) {
      if ($function) {
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
        $value = isset($path_map[$index]) ? $path_map[$index] : '';
        if (is_array($function)) {
          // Set up arguments for the load function. These were pulled from
          // 'load arguments' in the hook_menu() entry, but they need
          // some processing. In this case the $function is the key to the
          // load_function array, and the value is the list of arguments.
          list($function, $args) = each($function);

          // Some arguments are placeholders for dynamic items to process.
          foreach ($args as $i => $arg) {
            if ($arg == '%index') {
              // Pass on argument index to the load function, so multiple
              // occurances of the same placeholder can be identified.
              $args[$i] = $index;
            }
            if ($arg == '%map') {
              // Pass on menu map by reference. The accepting function must
              // also declare this as a reference if it wants to modify
              // the map.
              $args[$i] = &$map;
            }
          }
          array_unshift($args, $value);
          $return = call_user_func_array($function, $args);
        }
        else {
          $return = $function($value);
        }
367 368
        // If callback returned an error or there is no callback, trigger 404.
        if ($return === FALSE) {
369
          $item['access'] = FALSE;
370
          $map = FALSE;
371
          return FALSE;
372 373 374 375 376
        }
        $map[$index] = $return;
      }
    }
  }
377 378 379 380 381 382 383
  return TRUE;
}

/**
 * Check access to a menu item using the access callback
 *
 * @param $item
384
 *   A menu router or menu link item
385 386 387
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
388
 *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
389 390
 */
function _menu_check_access(&$item, $map) {
391 392
  // Determine access callback, which will decide whether or not the current
  // user has access to this path.
393
  $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']);
394
  // Check for a TRUE or FALSE value.
395
  if (is_numeric($callback)) {
396
    $item['access'] = (bool)$callback;
Dries's avatar
Dries committed
397
  }
398
  else {
399
    $arguments = menu_unserialize($item['access_arguments'], $map);
400 401 402
    // As call_user_func_array is quite slow and user_access is a very common
    // callback, it is worth making a special case for it.
    if ($callback == 'user_access') {
403
      $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
404 405
    }
    else {
406
      $item['access'] = call_user_func_array($callback, $arguments);
407
    }
Dries's avatar
Dries committed
408
  }
409
}
410

411 412 413
/**
 * Localize the item title using t() or another callback.
 */
414
function _menu_item_localize(&$item, $map) {
415 416 417
  // Translate the title to allow storage of English title strings in the
  // database, yet display of them in the language required by the current
  // user.
418
  $callback = $item['title_callback'];
419 420 421
  // t() is a special case. Since it is used very close to all the time,
  // we handle it directly instead of using indirect, slower methods.
  if ($callback == 't') {
422 423
    if (empty($item['title_arguments'])) {
      $item['title'] = t($item['title']);
424 425
    }
    else {
426
      $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
427 428 429
    }
  }
  else {
430 431
    if (empty($item['title_arguments'])) {
      $item['title'] = $callback($item['title']);
432 433
    }
    else {
434
      $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
435 436 437 438
    }
  }

  // Translate description, see the motivation above.
439 440
  if (!empty($item['description'])) {
    $item['description'] = t($item['description']);
441
  }
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
}

/**
 * Handles dynamic path translation and menu access control.
 *
 * When a user arrives on a page such as node/5, this function determines
 * what "5" corresponds to, by inspecting the page's menu path definition,
 * node/%node. This will call node_load(5) to load the corresponding node
 * object.
 *
 * It also works in reverse, to allow the display of tabs and menu items which
 * contain these dynamic arguments, translating node/%node to node/5.
 *
 * Translation of menu item titles and descriptions are done here to
 * allow for storage of English strings in the database, and translation
 * to the language required to generate the current page
 *
459 460
 * @param $router_item
 *   A menu router item
461 462 463
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @param $to_arg
464
 *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
465 466 467
 *   path from the menu table, for example tabs.
 * @return
 *   Returns the map with objects loaded as defined in the
468 469
 *   $item['load_functions. $item['access'] becomes TRUE if the item is
 *   accessible, FALSE otherwise. $item['href'] is set according to the map.
470 471 472
 *   If an error occurs during calling the load_functions (like trying to load
 *   a non existing node) then this function return FALSE.
 */
473
function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
474
  $path_map = $map;
475
  if (!_menu_load_objects($router_item, $map)) {
476
    // An error occurred loading an object.
477
    $router_item['access'] = FALSE;
478 479 480
    return FALSE;
  }
  if ($to_arg) {
481
    _menu_link_map_translate($path_map, $router_item['to_arg_functions']);
482 483 484
  }

  // Generate the link path for the page request or local tasks.
485 486
  $link_map = explode('/', $router_item['path']);
  for ($i = 0; $i < $router_item['number_parts']; $i++) {
487 488 489 490
    if ($link_map[$i] == '%') {
      $link_map[$i] = $path_map[$i];
    }
  }
491
  $router_item['href'] = implode('/', $link_map);
492
  $router_item['options'] = array();
493
  _menu_check_access($router_item, $map);
494

495
  _menu_item_localize($router_item, $map);
496 497 498 499 500 501 502 503 504 505 506 507

  return $map;
}

/**
 * This function translates the path elements in the map using any to_arg
 * helper function. These functions take an argument and return an object.
 * See http://drupal.org/node/109153 for more information.
 *
 * @param map
 *   An array of path arguments (ex: array('node', '5'))
 * @param $to_arg_functions
508
 *   An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
509 510 511 512 513 514
 */
function _menu_link_map_translate(&$map, $to_arg_functions) {
  if ($to_arg_functions) {
    $to_arg_functions = unserialize($to_arg_functions);
    foreach ($to_arg_functions as $index => $function) {
      // Translate place-holders into real values.
515
      $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
516 517 518 519 520 521 522 523 524 525
      if (!empty($map[$index]) || isset($arg)) {
        $map[$index] = $arg;
      }
      else {
        unset($map[$index]);
      }
    }
  }
}

526 527 528 529
function menu_tail_to_arg($arg, $map, $index) {
  return implode('/', array_slice($map, $index));
}

530 531 532 533 534
/**
 * This function is similar to _menu_translate() but does link-specific
 * preparation such as always calling to_arg functions
 *
 * @param $item
535
 *   A menu link
536 537
 * @return
 *   Returns the map of path arguments with objects loaded as defined in the
538 539 540 541
 *   $item['load_functions'].
 *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
 *   $item['href'] is generated from link_path, possibly by to_arg functions.
 *   $item['title'] is generated from link_title, and may be localized.
542 543
 */
function _menu_link_translate(&$item) {
544 545
  if ($item['external']) {
    $item['access'] = 1;
546
    $map = array();
547 548
    $item['href'] = $item['link_path'];
    $item['title'] = $item['link_title'];
549 550
  }
  else {
551 552 553
    $map = explode('/', $item['link_path']);
    _menu_link_map_translate($map, $item['to_arg_functions']);
    $item['href'] = implode('/', $map);
554

555
    // Note - skip callbacks without real values for their arguments.
556 557
    if (strpos($item['href'], '%') !== FALSE) {
      $item['access'] = FALSE;
558 559
      return FALSE;
    }
560
    // menu_tree_check_access() may set this ahead of time for links to nodes.
561 562
    if (!isset($item['access'])) {
      if (!_menu_load_objects($item, $map)) {
563
        // An error occured loading an object.
564 565 566
        $item['access'] = FALSE;
        return FALSE;
      }
567 568 569
      _menu_check_access($item, $map);
    }
    // If the link title matches that of a router item, localize it.
570 571
    if (!empty($item['title']) && (($item['title'] == $item['link_title']) || ($item['title_callback'] != 't'))) {
      _menu_item_localize($item, $map);
572 573 574
    }
    else {
      $item['title'] = $item['link_title'];
575 576
    }
  }
577
  $item['options'] = unserialize($item['options']);
578

579
  return $map;
580 581
}

582
/**
583 584 585 586 587
 * Render a menu tree based on the current path.
 *
 * The tree is expanded based on the current path and dynamic paths are also
 * changed according to the defined to_arg functions (for example the 'My account'
 * link is changed from user/% to a link with the current user's uid).
588 589 590 591 592 593 594 595 596 597
 *
 * @param $menu_name
 *   The name of the menu.
 * @return
 *   The rendered HTML of that menu on the current page.
 */
function menu_tree($menu_name = 'navigation') {
  static $menu_output = array();

  if (!isset($menu_output[$menu_name])) {
598
    $tree = menu_tree_page_data($menu_name);
599 600 601 602 603
    $menu_output[$menu_name] = menu_tree_output($tree);
  }
  return $menu_output[$menu_name];
}

604
/**
605
 * Returns a rendered menu tree.
606 607 608 609 610
 *
 * @param $tree
 *   A data structure representing the tree as returned from menu_tree_data.
 * @return
 *   The rendered HTML of that data structure.
611
 */
612 613
function menu_tree_output($tree) {
  $output = '';
614
  $items = array();
615

616
  // Pull out just the menu items we are going to render so that we
617
  // get an accurate count for the first/last classes.
618
  foreach ($tree as $data) {
619
    if (!$data['link']['hidden']) {
620 621 622
      $items[] = $data;
    }
  }
623

624 625 626 627 628 629 630 631 632 633 634 635 636 637 638
  $num_items = count($items);
  foreach ($items as $i => $data) {
    $extra_class = NULL;
    if ($i == 0) {
      $extra_class = 'first';
    }
    if ($i == $num_items - 1) {
      $extra_class = 'last';
    }
    $link = theme('menu_item_link', $data['link']);
    if ($data['below']) {
      $output .= theme('menu_item', $link, $data['link']['has_children'], menu_tree_output($data['below']), $data['link']['in_active_trail'], $extra_class);
    }
    else {
      $output .= theme('menu_item', $link, $data['link']['has_children'], '', $data['link']['in_active_trail'], $extra_class);
639
    }
640 641 642 643
  }
  return $output ? theme('menu_tree', $output) : '';
}

644
/**
645 646 647 648
 * Get the data structure representing a named menu tree.
 *
 * Since this can be the full tree including hidden items, the data returned
 * may be used for generating an an admin interface or a select.
649 650 651 652 653 654 655 656 657 658
 *
 * @param $menu_name
 *   The named menu links to return
 * @param $item
 *   A fully loaded menu link, or NULL.  If a link is supplied, only the
 *   path to root will be included in the returned tree- as if this link
 *   represented the current page in a visible menu.
 * @return
 *   An tree of menu links in an array, in the order they should be rendered.
 */
659
function menu_tree_all_data($menu_name = 'navigation', $item = NULL) {
660 661
  static $tree = array();

662
  // Use $mlid as a flag for whether the data being loaded is for the whole tree.
663
  $mlid = isset($item['mlid']) ? $item['mlid'] : 0;
664
  // Generate the cache ID.
665
  $cid = 'links:'. $menu_name .':all:'. $mlid;
666 667

  if (!isset($tree[$cid])) {
668
    // If the static variable doesn't have the data, check {cache_menu}.
669 670
    $cache = cache_get($cid, 'cache_menu');
    if ($cache && isset($cache->data)) {
671
      $data = $cache->data;
672 673
    }
    else {
674
      // Build and run the query, and build the tree.
675
      if ($mlid) {
676 677
        // The tree is for a single item, so we need to match the values in its
        // p columns and 0 (the top level) with the plid values of other links.
678 679 680 681
        $args = array(0);
        for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
          $args[] = $item["p$i"];
        }
682 683 684 685 686 687 688
        $args = array_unique($args);
        $placeholders = implode(', ', array_fill(0, count($args), '%d'));
        $where = ' AND ml.plid IN ('. $placeholders .')';
        $parents = $args;
        $parents[] = $item['mlid'];
      }
      else {
689
        // Get all links in this menu.
690 691 692 693 694
        $where = '';
        $args = array();
        $parents = array();
      }
      array_unshift($args, $menu_name);
695
      // Select the links from the table, and recursively build the tree.  We
696
      // LEFT JOIN since there is no match in {menu_router} for an external
697
      // link.
698 699
      $data['tree'] = menu_tree_data(db_query("
        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, ml.*
700
        FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
701 702
        WHERE ml.menu_name = '%s'". $where ."
        ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC", $args), $parents);
703 704
      $data['node_links'] = array();
      menu_tree_collect_node_links($data['tree'], $data['node_links']);
705
      // Cache the data.
706
      cache_set($cid, $data, 'cache_menu');
707
    }
708
    // Check access for the current user to each item in the tree.
709
    menu_tree_check_access($data['tree'], $data['node_links']);
710
    $tree[$cid] = $data['tree'];
711 712 713 714 715
  }

  return $tree[$cid];
}

716
/**
717 718 719
 * Get the data structure representing a named menu tree, based on the current page.
 *
 * The tree order is maintained by storing each parent in an individual
720 721 722 723 724 725 726 727
 * field, see http://drupal.org/node/141866 for more.
 *
 * @param $menu_name
 *   The named menu links to return
 * @return
 *   An array of menu links, in the order they should be rendered. The array
 *   is a list of associative arrays -- these have two keys, link and below.
 *   link is a menu item, ready for theming as a link. Below represents the
728 729
 *   submenu below the link if there is one, and it is a subtree that has the
 *   same structure described for the top-level array.
730
 */
731
function menu_tree_page_data($menu_name = 'navigation') {
732 733
  static $tree = array();

734
  // Load the menu item corresponding to the current page.
735
  if ($item = menu_get_item()) {
736
    // Generate the cache ID.
737
    $cid = 'links:'. $menu_name .':page:'. $item['href'] .':'. (int)$item['access'];
738

739
    if (!isset($tree[$cid])) {
740
      // If the static variable doesn't have the data, check {cache_menu}.
741 742
      $cache = cache_get($cid, 'cache_menu');
      if ($cache && isset($cache->data)) {
743
        $data = $cache->data;
744 745
      }
      else {
746
        // Build and run the query, and build the tree.
747
        if ($item['access']) {
748
          // Check whether a menu link exists that corresponds to the current path.
749
          $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['href']));
750

751
          if (empty($parents)) {
752
            // If no link exists, we may be on a local task that's not in the links.
753
            // TODO: Handle the case like a local task on a specific node in the menu.
754
            $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']));
755
          }
756
          // We always want all the top-level links with plid == 0.
757 758
          $parents[] = '0';

759 760
          // Use array_values() so that the indices are numeric for array_merge().
          $args = $parents = array_unique(array_values($parents));
761 762
          $placeholders = implode(', ', array_fill(0, count($args), '%d'));
          $expanded = variable_get('menu_expanded', array());
763
          // Check whether the current menu has any links set to be expanded.
764
          if (in_array($menu_name, $expanded)) {
765 766
            // Collect all the links set to be expanded, and then add all of
            // their children to the list as well.
767
            do {
768
              $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));
769
              $num_rows = FALSE;
770 771
              while ($item = db_fetch_array($result)) {
                $args[] = $item['mlid'];
772
                $num_rows = TRUE;
773 774
              }
              $placeholders = implode(', ', array_fill(0, count($args), '%d'));
775
            } while ($num_rows);
776 777 778 779
          }
          array_unshift($args, $menu_name);
        }
        else {
780 781
          // Show only the top-level menu items when access is denied.
          $args = array($menu_name, '0');
782 783 784
          $placeholders = '%d';
          $parents = array();
        }
785 786
        // Select the links from the table, and recursively build the tree. We
        // LEFT JOIN since there is no match in {menu_router} for an external
787
        // link.
788 789
        $data['tree'] = menu_tree_data(db_query("
          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, ml.*
790
          FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
791 792
          WHERE ml.menu_name = '%s' AND ml.plid IN (". $placeholders .")
          ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC", $args), $parents);
793 794
        $data['node_links'] = array();
        menu_tree_collect_node_links($data['tree'], $data['node_links']);
795
        // Cache the data.
796
        cache_set($cid, $data, 'cache_menu');
797
      }
798
      // Check access for the current user to each item in the tree.
799 800
      menu_tree_check_access($data['tree'], $data['node_links']);
      $tree[$cid] = $data['tree'];
801
    }
802
    return $tree[$cid];
803
  }
804 805

  return array();
806 807
}

808 809 810 811 812 813 814 815 816
/**
 * Recursive helper function - collect node links.
 */
function menu_tree_collect_node_links(&$tree, &$node_links) {

  foreach ($tree as $key => $v) {
    if ($tree[$key]['link']['router_path'] == 'node/%') {
      $nid = substr($tree[$key]['link']['link_path'], 5);
      if (is_numeric($nid)) {
817
        $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link'];
818 819 820 821
        $tree[$key]['link']['access'] = FALSE;
      }
    }
    if ($tree[$key]['below']) {
822
      menu_tree_collect_node_links($tree[$key]['below'], $node_links);
823 824 825 826
    }
  }
}

827 828 829
/**
 * Check access and perform other dynamic operations for each link in the tree.
 */
830
function menu_tree_check_access(&$tree, $node_links = array()) {
831 832 833 834

  if ($node_links) {
    // Use db_rewrite_sql to evaluate view access without loading each full node.
    $nids = array_keys($node_links);
835
    $placeholders = '%d'. str_repeat(', %d', count($nids) - 1);
836 837
    $result = db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.nid IN (". $placeholders .")"), $nids);
    while ($node = db_fetch_array($result)) {
838 839 840 841
      $nid = $node['nid'];
      foreach ($node_links[$nid] as $mlid => $link) {
        $node_links[$nid][$mlid]['access'] = TRUE;
      }
842 843
    }
  }
844
  _menu_tree_check_access($tree);
845
  return;
846 847 848 849 850
}

/**
 * Recursive helper function for menu_tree_check_access()
 */
851
function _menu_tree_check_access(&$tree) {
852
  $new_tree = array();
853 854
  foreach ($tree as $key => $v) {
    $item = &$tree[$key]['link'];
855
    _menu_link_translate($item);
856 857
    if ($item['access']) {
      if ($tree[$key]['below']) {
858
        _menu_tree_check_access($tree[$key]['below']);
859 860 861 862 863
      }
      // The weights are made a uniform 5 digits by adding 50000 as an offset.
      // After _menu_link_translate(), $item['title'] has the localized link title.
      // Adding the mlid to the end of the index insures that it is unique.
      $new_tree[(50000 + $item['weight']) .' '. $item['title'] .' '. $item['mlid']] = $tree[$key];
864 865
    }
  }
866 867 868
  // Sort siblings in the tree based on the weights and localized titles.
  ksort($new_tree);
  $tree = $new_tree;
869
}
870

871
/**
872
 * Build the data representing a menu tree.
873 874 875
 *
 * @param $result
 *   The database result.
876 877 878
 * @param $parents
 *   An array of the plid values that represent the path from the current page
 *   to the root of the menu tree.
879 880 881
 * @param $depth
 *   The depth of the current menu tree.
 * @return
882 883 884 885 886 887 888 889 890 891 892 893 894 895
 *   See menu_tree_page_data for a description of the data structure.
 */
function menu_tree_data($result = NULL, $parents = array(), $depth = 1) {

  list(, $tree) = _menu_tree_data($result, $parents, $depth);
  return $tree;
}

/**
 * Recursive helper function to build the data representing a menu tree.
 *
 * The function is a bit complex because the rendering of an item depends on
 * the next menu item. So we are always rendering the element previously
 * processed not the current one.
896
 */
897
function _menu_tree_data($result, $parents, $depth, $previous_element = '') {
898
  $remnant = NULL;
899
  $tree = array();
900
  while ($item = db_fetch_array($result)) {
901 902
    // We need to determine if we're on the path to root so we can later build
    // the correct active trail and breadcrumb.
903
    $item['in_active_trail'] = in_array($item['mlid'], $parents);
904 905

    $index = $previous_element ? ($previous_element['mlid']) : '';
906
    // The current item is the first in a new submenu.
907
    if ($item['depth'] > $depth) {
908
      // _menu_tree returns an item and the menu tree structure.
909
      list($item, $below) = _menu_tree_data($result, $parents, $item['depth'], $item);
910 911 912 913 914
      $tree[$index] = array(
        'link' => $previous_element,
        'below' => $below,
      );
      // We need to fall back one level.
915
      if (!isset($item) || $item['depth'] < $depth) {
916 917
        return array($item, $tree);
      }
918
      // This will be the link to be output in the next iteration.
919
      $previous_element = $item;
920
    }
921
    // We are at the same depth, so we use the previous element.
922
    elseif ($item['depth'] == $depth) {
923 924
      if ($previous_element) {
        // Only the first time.
925 926
        $tree[$index] = array(
          'link' => $previous_element,
927
          'below' => FALSE,
928 929
        );
      }
930
      // This will be the link to be output in the next iteration.
931
      $previous_element = $item;
932
    }
933
    // The submenu ended with the previous item, so pass back the current item.
934
    else {
935
      $remnant = $item;
936
      break;
937
    }
938
  }
939
  if ($previous_element) {
940
    // We have one more link dangling.
941
    $tree[$previous_element['mlid']] = array(
942
      'link' => $previous_element,
943
      'below' => FALSE,
944
    );
945 946
  }
  return array($remnant, $tree);
947 948
}

Dries's avatar
Dries committed
949
/**
950
 * Generate the HTML output for a single menu link.
Dries's avatar
Dries committed
951
 */
952
function theme_menu_item_link($link) {
953 954 955 956
  if (empty($link['options'])) {
    $link['options'] = array();
  }

957
  return l($link['title'], $link['href'], $link['options']);
Dries's avatar
Dries committed
958 959
}

960
/**
961
 * Generate the HTML output for a menu tree
962
 */
963 964 965 966 967 968 969
function theme_menu_tree($tree) {
  return '<ul class="menu">'. $tree .'</ul>';
}

/**
 * Generate the HTML output for a menu item and submenu.
 */
970
function theme_menu_item($link, $has_children, $menu = '', $in_active_trail = FALSE, $extra_class = NULL) {
971
  $class = ($menu ? 'expanded' : ($has_children ? 'collapsed' : 'leaf'));
972 973 974
  if (!empty($extra_class)) {
    $class .= ' '. $extra_class;
  }
975 976 977 978
  if ($in_active_trail) {
    $class .= ' active-trail';
  }
  return '<li class="'. $class .'">'. $link . $menu .'</li>'."\n";
979 980
}

981 982 983
/**
 * Generate the HTML output for a single local task link.
 */
984
function theme_menu_local_task($link, $active = FALSE) {
985
  return '<li '. ($active ? 'class="active" ' : '') .'>'. $link .'</li>';
986
}
987

988 989 990 991 992 993 994 995
/**
 * Generates elements for the $arg array in the help hook.
 */
function drupal_help_arg($arg = array()) {
  // Note - the number of empty elements should be > MENU_MAX_PARTS.
  return $arg + array('', '', '', '', '', '', '', '', '', '', '', '');
}

996
/**
997 998
 * Returns the help associated with the active menu item.
 */
999
function menu_get_active_help() {
Dries's avatar
Dries committed
1000
  $output = '';
1001
  $router_path = menu_tab_root_path();
1002

1003 1004
  $arg = drupal_help_arg(arg(NULL));
  $empty_arg = drupal_help_arg();
1005

1006 1007
  foreach (module_list() as $name) {
    if (module_hook($name, 'help')) {
1008 1009 1010
      // Lookup help for this path.
      if ($help = module_invoke($name, 'help', $router_path, $arg)) {
        $output .= $help ."\n";
1011
      }
Gábor Hojtsy's avatar