menu.inc 88.1 KB
Newer Older
1
<?php
Kjartan's avatar
Kjartan committed
2

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

Dries's avatar
Dries committed
8 9 10
/**
 * @defgroup menu Menu system
 * @{
11
 * Define the navigation menus, and route page requests to code based on URLs.
12 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
 *
 * 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.
 *
Steven Wittens's avatar
Steven Wittens committed
39
 * The found callback function is called with any arguments specified
40
 * in the "page arguments" attribute of its menu item. The
Steven Wittens's avatar
Steven Wittens committed
41 42 43 44
 * 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.
45 46 47 48
 *
 * For an illustration of this process, see page_example.module.
 *
 * Access to the callback functions is also protected by the menu system.
49 50
 * The "access callback" with an optional "access arguments" of each menu
 * item is called before the page callback proceeds. If this returns TRUE,
51 52 53
 * then access is granted; if FALSE, then access is denied. Default local task
 * menu items (see next paragraph) may omit this attribute to use the value
 * provided by the parent 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
 * @defgroup menu_flags 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 84 85 86
define('MENU_LINKS_TO_PARENT', 0x0008);
define('MENU_MODIFIED_BY_ADMIN', 0x0020);
define('MENU_CREATED_BY_ADMIN', 0x0040);
define('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 96
 * Definitions for various menu item types.
 *
97 98 99
 * Menu item definitions provide one of these constants, which are shortcuts for
 * combinations of the above flags.
 */
Dries's avatar
Dries committed
100

Dries's avatar
Dries committed
101 102
/**
 * Normal menu items show up in the menu tree and can be moved/hidden by
103 104
 * 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
105
 */
106
define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);
107

Dries's avatar
Dries committed
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.
Dries's avatar
Dries committed
111 112
 */
define('MENU_CALLBACK', MENU_VISIBLE_IN_BREADCRUMB);
113

Dries's avatar
Dries committed
114
/**
115 116
 * 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.
117 118
 * Note for the value: 0x0010 was a flag which is no longer used, but this way
 * the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate.
Dries's avatar
Dries committed
119
 */
120
define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010);
Dries's avatar
Dries committed
121 122

/**
123 124 125
 * 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
126 127 128
 */
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK);

129 130 131 132 133 134
/**
 * 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
135
/**
136
 * @} End of "Menu item types".
137 138 139
 */

/**
140
 * @defgroup menu_status_codes Menu status codes
141
 * @{
Dries's avatar
Dries committed
142 143
 * Status codes for menu callbacks.
 */
144

Dries's avatar
Dries committed
145 146 147
define('MENU_FOUND', 1);
define('MENU_NOT_FOUND', 2);
define('MENU_ACCESS_DENIED', 3);
148
define('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
 */
163
define('MENU_MAX_PARTS', 7);
164

165 166

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

171 172

/**
173
 * @} End of "Menu tree parameters".
174 175
 */

176
/**
177 178 179
 * Returns the ancestors (and relevant placeholders) for any given path.
 *
 * For example, the ancestors of node/12345/edit are:
180 181 182 183 184 185 186
 * - node/12345/edit
 * - node/12345/%
 * - node/%/edit
 * - node/%/%
 * - node/12345
 * - node/%
 * - node
187 188 189 190 191
 *
 * 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
192 193 194
 * 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().
195 196 197 198 199 200
 *
 * @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
201
 *   simply contain as many '%s' as the ancestors.
202 203
 */
function menu_get_ancestors($parts) {
204
  $number_parts = count($parts);
205 206
  $placeholders = array();
  $ancestors = array();
207 208 209 210 211 212 213 214 215 216 217 218 219
  $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;
    }
220 221 222 223 224 225 226 227 228 229 230
    $current = '';
    for ($j = $length; $j >= 0; $j--) {
      if ($i & (1 << $j)) {
        $current .= $parts[$length - $j];
      }
      else {
        $current .= '%';
      }
      if ($j) {
        $current .= '/';
      }
231
    }
232 233
    $placeholders[] = "'%s'";
    $ancestors[] = $current;
234
  }
235
  return array($ancestors, $placeholders);
236 237 238
}

/**
239 240 241 242
 * 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.
243
 *
244
 * Integer values are mapped according to the $map parameter. For
245 246 247 248 249
 * 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').
250
 *
251 252 253 254
 * @param @data
 *   A serialized array.
 * @param @map
 *   An array of potential replacements.
255
 * @return
256
 *   The $data array unserialized and mapped.
257
 */
258 259 260 261 262 263 264 265
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;
266
  }
267 268
  else {
    return array();
269 270 271
  }
}

272 273


274
/**
275
 * Replaces the statically cached item for a given path.
276
 *
277
 * @param $path
278 279 280 281 282 283
 *   The path.
 * @param $router_item
 *   The router item. Usually you take a router entry from menu_get_item and
 *   set it back either modified or to a different path. This lets you modify the
 *   navigation block, the page title, the breadcrumb and the page help in one
 *   call.
284
 */
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
function menu_set_item($path, $router_item) {
  menu_get_item($path, $router_item);
}

/**
 * Get a router item.
 *
 * @param $path
 *   The path, for example node/5. The function will find the corresponding
 *   node/% item and return that.
 * @param $router_item
 *   Internal use only.
 * @return
 *   The router item, an associate array corresponding to one row in the
 *   menu_router table. The value of key map holds the loaded objects. The
 *   value of key access is TRUE if the current user can access this page.
 *   The values for key title, page_arguments, access_arguments will be
 *   filled in based on the database values and the objects loaded.
 */
function menu_get_item($path = NULL, $router_item = NULL) {
305
  static $router_items;
306 307 308
  if (!isset($path)) {
    $path = $_GET['q'];
  }
309 310 311
  if (isset($router_item)) {
    $router_items[$path] = $router_item;
  }
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 318
    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);
319
      if ($map === FALSE) {
320
        $router_items[$path] = FALSE;
321
        return FALSE;
322
      }
323 324 325
      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
326 327
      }
    }
328
    $router_items[$path] = $router_item;
Dries's avatar
Dries committed
329
  }
330
  return $router_items[$path];
Dries's avatar
Dries committed
331 332 333
}

/**
334
 * Execute the page callback associated with the current path
Dries's avatar
Dries committed
335
 */
336
function menu_execute_active_handler($path = NULL) {
337 338
  if (_menu_site_is_offline()) {
    return MENU_SITE_OFFLINE;
339
  }
340 341 342
  // Rebuild if we know it's needed, or if the menu masks are missing which
  // occurs rarely, likely due to a race condition of multiple rebuilds.
  if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) {
343 344
    menu_rebuild();
  }
345
  if ($router_item = menu_get_item($path)) {
346 347 348
    if ($router_item['access']) {
      if ($router_item['file']) {
        require_once($router_item['file']);
349
      }
350
      return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
351 352 353 354
    }
    else {
      return MENU_ACCESS_DENIED;
    }
355
  }
356 357
  return MENU_NOT_FOUND;
}
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 365 366
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
367 368
 *   Returns TRUE for success, FALSE if an object cannot be loaded.
 *   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 371
 *   $item['load_functions'] array.
 *   $item['access'] is set to FALSE if an object cannot be loaded.
372
 */
373 374 375 376 377 378
function _menu_load_objects(&$item, &$map) {
  if ($load_functions = $item['load_functions']) {
    // If someone calls this function twice, then unserialize will fail.
    if ($load_functions_unserialized = unserialize($load_functions)) {
      $load_functions = $load_functions_unserialized;
    }
379 380 381
    $path_map = $map;
    foreach ($load_functions as $index => $function) {
      if ($function) {
382 383 384 385 386 387 388
        $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);
389
          $load_functions[$index] = $function;
390 391 392

          // Some arguments are placeholders for dynamic items to process.
          foreach ($args as $i => $arg) {
393
            if ($arg === '%index') {
394 395 396 397
              // Pass on argument index to the load function, so multiple
              // occurances of the same placeholder can be identified.
              $args[$i] = $index;
            }
398
            if ($arg === '%map') {
399 400 401 402 403
              // 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;
            }
404 405 406
            if (is_int($arg)) {
              $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : '';
            }
407 408 409 410 411 412 413
          }
          array_unshift($args, $value);
          $return = call_user_func_array($function, $args);
        }
        else {
          $return = $function($value);
        }
414 415
        // If callback returned an error or there is no callback, trigger 404.
        if ($return === FALSE) {
416
          $item['access'] = FALSE;
417
          $map = FALSE;
418
          return FALSE;
419 420 421 422
        }
        $map[$index] = $return;
      }
    }
423
    $item['load_functions'] = $load_functions;
424
  }
425 426 427 428 429 430 431
  return TRUE;
}

/**
 * Check access to a menu item using the access callback
 *
 * @param $item
432
 *   A menu router or menu link item
433 434 435
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
436
 *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
437 438
 */
function _menu_check_access(&$item, $map) {
439 440
  // Determine access callback, which will decide whether or not the current
  // 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;
Dries's avatar
Dries committed
445
  }
446
  else {
447
    $arguments = menu_unserialize($item['access_arguments'], $map);
448 449 450
    // 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') {
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
    }
Dries's avatar
Dries committed
456
  }
457
}
458

459
/**
460
 * Localize the router item title using t() or another callback.
461
 *
462 463 464 465 466 467 468 469 470 471 472 473 474
 * Translate the title and description to allow storage of English title
 * strings in the database, yet display of them in the language required
 * by the current user.
 *
 * @param $item
 *   A menu router item or a menu link item.
 * @param $map
 *   The path as an array with objects already replaced. E.g., for path
 *   node/123 $map would be array('node', $node) where $node is the node
 *   object for node 123.
 * @param $link_translate
 *   TRUE if we are translating a menu link item; FALSE if we are
 *   translating a menu router item.
475 476 477
 * @return
 *   No return value.
 *   $item['title'] is localized according to $item['title_callback'].
478
 *   If an item's callback is check_plain(), $item['options']['html'] becomes
479 480
 *   TRUE.
 *   $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 484
 */
function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
485
  $callback = $item['title_callback'];
486
  $item['localized_options'] = $item['options'];
487 488 489 490 491 492 493
  // If we are translating the title of a menu link, and its title is the same
  // as the corresponding router item, then we can use the title information
  // from the router. If it's customized, then we need to use the link title
  // itself; can't localize.
  // If we are translating a router item (tabs, page, breadcrumb), then we
  // can always use the information from the router item.
  if (!$link_translate || ($item['title'] == $item['link_title'])) {
494 495 496 497 498 499 500 501 502
    // 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') {
      if (empty($item['title_arguments'])) {
        $item['title'] = t($item['title']);
      }
      else {
        $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
      }
503
    }
504 505 506 507 508 509 510
    elseif ($callback) {
      if (empty($item['title_arguments'])) {
        $item['title'] = $callback($item['title']);
      }
      else {
        $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
      }
511 512
      // Avoid calling check_plain again on l() function.
      if ($callback == 'check_plain') {
513
        $item['localized_options']['html'] = TRUE;
514
      }
515 516
    }
  }
517 518
  elseif ($link_translate) {
    $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 533 534 535 536 537 538 539 540 541 542 543 544 545
}

/**
 * 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
 *
546 547
 * @param $router_item
 *   A menu router item
548 549 550
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @param $to_arg
551
 *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
552 553 554
 *   path from the menu table, for example tabs.
 * @return
 *   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 558 559
 *   If an error occurs during calling the load_functions (like trying to load
 *   a non existing node) then this function return FALSE.
 */
560
function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
561 562 563 564 565 566
  if ($to_arg) {
    // Fill in missing path elements, such as the current uid.
    _menu_link_map_translate($map, $router_item['to_arg_functions']);
  }
  // The $path_map saves the pieces of the path as strings, while elements in
  // $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 572 573 574
    return FALSE;
  }

  // Generate the link path for the page request or local tasks.
575 576
  $link_map = explode('/', $router_item['path']);
  for ($i = 0; $i < $router_item['number_parts']; $i++) {
577 578 579 580
    if ($link_map[$i] == '%') {
      $link_map[$i] = $path_map[$i];
    }
  }
581
  $router_item['href'] = implode('/', $link_map);
582
  $router_item['options'] = array();
583
  _menu_check_access($router_item, $map);
584 585 586 587 588
  
  // For performance, don't localize an item the user can't access.
  if ($router_item['access']) {
    _menu_item_localize($router_item, $map);
  }
589 590 591 592 593 594 595 596 597 598 599 600

  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
601
 *   An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
602 603 604 605 606 607
 */
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.
608
      $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
609 610 611 612 613 614 615 616 617 618
      if (!empty($map[$index]) || isset($arg)) {
        $map[$index] = $arg;
      }
      else {
        unset($map[$index]);
      }
    }
  }
}

619 620 621 622
function menu_tail_to_arg($arg, $map, $index) {
  return implode('/', array_slice($map, $index));
}

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 630
 * @return
 *   Returns the map of path arguments with objects loaded as defined in the
631 632 633 634 635 636
 *   $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.
 *   - $item['options'] is unserialized; it is also changed within the call
 *     here to $item['localized_options'] by _menu_item_localize().
637 638
 */
function _menu_link_translate(&$item) {
639
  $item['options'] = unserialize($item['options']);
640 641
  if ($item['external']) {
    $item['access'] = 1;
642
    $map = array();
643 644
    $item['href'] = $item['link_path'];
    $item['title'] = $item['link_title'];
645
    $item['localized_options'] = $item['options'];
646 647
  }
  else {
648 649 650
    $map = explode('/', $item['link_path']);
    _menu_link_map_translate($map, $item['to_arg_functions']);
    $item['href'] = implode('/', $map);
651

652
    // Note - skip callbacks without real values for their arguments.
653 654
    if (strpos($item['href'], '%') !== FALSE) {
      $item['access'] = FALSE;
655 656
      return FALSE;
    }
657
    // menu_tree_check_access() may set this ahead of time for links to nodes.
658 659
    if (!isset($item['access'])) {
      if (!_menu_load_objects($item, $map)) {
660
        // An error occurred loading an object.
661 662 663
        $item['access'] = FALSE;
        return FALSE;
      }
664 665
      _menu_check_access($item, $map);
    }
666 667 668 669
    // For performance, don't localize a link the user can't access.
    if ($item['access']) {
      _menu_item_localize($item, $map, TRUE);
    }
670
  }
671

672 673 674 675 676 677
  // Allow other customizations - e.g. adding a page-specific query string to the
  // options array. For performance reasons we only invoke this hook if the link
  // has the 'alter' flag set in the options array.
  if (!empty($item['options']['alter'])) {
    drupal_alter('translated_menu_link', $item, $map);
  }
678

679
  return $map;
680 681
}

682 683 684 685 686
/**
 * Get a loaded object from a router item.
 *
 * menu_get_object() will provide you the current node on paths like node/5,
 * node/5/revisions/48 etc. menu_get_object('user') will give you the user
687 688 689
 * account on user/5 etc. Note - this function should never be called within a
 * _to_arg function (like user_current_to_arg()) since this may result in an
 * infinite recursion.
690 691 692 693 694 695 696 697 698 699
 *
 * @param $type
 *   Type of the object. These appear in hook_menu definitons as %type. Core
 *   provides aggregator_feed, aggregator_category, contact, filter_format,
 *   forum_term, menu, menu_link, node, taxonomy_vocabulary, user. See the
 *   relevant {$type}_load function for more on each. Defaults to node.
 * @param $position
 *   The expected position for $type object. For node/%node this is 1, for
 *   comment/reply/%node this is 2. Defaults to 1.
 * @param $path
700
 *   See menu_get_item() for more on this. Defaults to the current path.
701 702 703 704 705 706 707 708
 */
function menu_get_object($type = 'node', $position = 1, $path = NULL) {
  $router_item = menu_get_item($path);
  if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type .'_load') {
    return $router_item['map'][$position];
  }
}

709
/**
710 711 712 713 714
 * 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).
715 716 717 718 719 720 721 722 723 724
 *
 * @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])) {
725
    $tree = menu_tree_page_data($menu_name);
726 727 728 729 730
    $menu_output[$menu_name] = menu_tree_output($tree);
  }
  return $menu_output[$menu_name];
}

731
/**
732
 * Returns a rendered menu tree.
733 734 735 736 737
 *
 * @param $tree
 *   A data structure representing the tree as returned from menu_tree_data.
 * @return
 *   The rendered HTML of that data structure.
738
 */
739 740
function menu_tree_output($tree) {
  $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 748 749
      $items[] = $data;
    }
  }
750

751 752
  $num_items = count($items);
  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 762 763 764 765 766
    $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);
767
    }
768 769 770 771
  }
  return $output ? theme('menu_tree', $output) : '';
}

772
/**
773 774 775 776
 * 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.
777 778 779 780 781 782 783 784 785 786
 *
 * @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.
 */
787
function menu_tree_all_data($menu_name = 'navigation', $item = NULL) {
788 789
  static $tree = array();

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 793
  // Generate a cache ID (cid) specific for this $menu_name and $item.
  $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 798
    $cache = cache_get($cid, 'cache_menu');
    if ($cache && isset($cache->data)) {
799 800 801 802 803 804
      // If the cache entry exists, it will just be the cid for the actual data.
      // This avoids duplication of large amounts of data.
      $cache = cache_get($cache->data, 'cache_menu');
      if ($cache && isset($cache->data)) {
        $data = $cache->data;
      }
805
    }
806 807
    // If the tree data was not in the cache, $data will be NULL.
    if (!isset($data)) {
808
      // Build and run the query, and build the tree.
809
      if ($mlid) {
810 811
        // 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.
812 813 814 815
        $args = array(0);
        for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
          $args[] = $item["p$i"];
        }
816 817 818 819 820 821 822
        $args = array_unique($args);
        $placeholders = implode(', ', array_fill(0, count($args), '%d'));
        $where = ' AND ml.plid IN ('. $placeholders .')';
        $parents = $args;
        $parents[] = $item['mlid'];
      }
      else {
823
        // Get all links in this menu.
824 825 826 827 828
        $where = '';
        $args = array();
        $parents = array();
      }
      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 836
        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);
837 838
      $data['node_links'] = array();
      menu_tree_collect_node_links($data['tree'], $data['node_links']);
839 840 841 842 843 844 845
      // Cache the data, if it is not already in the cache.
      $tree_cid = _menu_tree_cid($menu_name, $data);
      if (!cache_get($tree_cid, 'cache_menu')) {
        cache_set($tree_cid, $data, 'cache_menu');
      }
      // Cache the cid of the (shared) data using the menu and item-specific cid.
      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 853 854
  }

  return $tree[$cid];
}

855
/**
856 857 858
 * 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
859 860 861 862 863 864 865 866
 * 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
867 868
 *   submenu below the link if there is one, and it is a subtree that has the
 *   same structure described for the top-level array.
869
 */
870
function menu_tree_page_data($menu_name = 'navigation') {
871 872
  static $tree = array();

873
  // Load the menu item corresponding to the current page.
874
  if ($item = menu_get_item()) {
875 876
    // Generate a cache ID (cid) specific for this page.
    $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 881
      $cache = cache_get($cid, 'cache_menu');
      if ($cache && isset($cache->data)) {
882 883 884 885 886 887
        // If the cache entry exists, it will just be the cid for the actual data.
        // This avoids duplication of large amounts of data.
        $cache = cache_get($cache->data, 'cache_menu');
        if ($cache && isset($cache->data)) {
          $data = $cache->data;
        }
888
      }
889 890
      // If the tree data was not in the cache, $data will be NULL.
      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 895 896 897 898 899 900
          $args = array($menu_name, $item['href']);
          $placeholders = "'%s'";
          if (drupal_is_front_page()) {
            $args[] = '<front>';
            $placeholders .= ", '%s'";
          }
          $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 909
          $parents[] = '0';

910 911
          // Use array_values() so that the indices are numeric for array_merge().
          $args = $parents = array_unique(array_values($parents));
912 913
          $placeholders = implode(', ', array_fill(0, count($args), '%d'));
          $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 917
            // Collect all the links set to be expanded, and then add all of
            // 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 922
              while ($item = db_fetch_array($result)) {
                $args[] = $item['mlid'];
923
                $num_rows = TRUE;
924 925
              }
              $placeholders = implode(', ', array_fill(0, count($args), '%d'));
926
            } while ($num_rows);
927 928 929 930
          }
          array_unshift($args, $menu_name);
        }
        else {
931 932
          // Show only the top-level menu items when access is denied.
          $args = array($menu_name, '0');
933 934 935
          $placeholders = '%d';
          $parents = array();
        }
936 937
        // 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
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 943
          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);
944 945
        $data['node_links'] = array();
        menu_tree_collect_node_links($data['tree'], $data['node_links']);
946 947 948 949 950 951 952
        // Cache the data, if it is not already in the cache.
        $tree_cid = _menu_tree_cid($menu_name, $data);
        if (!cache_get($tree_cid, 'cache_menu')) {
          cache_set($tree_cid, $data, 'cache_menu');
        }
        // Cache the cid of the (shared) data using the page-specific cid.
        cache_set($cid, $tree_cid, 'cache_menu');
953
      }
954
      // Check access for the current user to each item in the tree.
955 956
      menu_tree_check_access($data['tree'], $data['node_links']);
      $tree[$cid] = $data['tree'];
957
    }
958
    return $tree[$cid];
959
  }
960 961

  return array();
962 963
}

964 965 966 967 968 969 970
/**
 * Helper function - compute the real cache ID for menu tree data.
 */
function _menu_tree_cid($menu_name, $data) {
  return 'links:'. $menu_name .':tree-data:'. md5(serialize($data));
}

971 972
/**
 * Recursive helper function - collect node links.
973 974 975 976 977
 *
 * @param $tree
 *   The menu tree you wish to collect node links from.
 * @param $node_links
 *   An array in which to store the collected node links.
978 979 980 981 982 983
 */
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)) {
984
        $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link'];
985 986 987 988
        $tree[$key]['link']['access'] = FALSE;
      }
    }
    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 998 999 1000 1001
 *
 * @param $tree
 *   The menu tree you wish to operate on.
 * @param $node_links
 *   A collection of node link references generated from $tree by
 *   menu_tree_collect_node_links().
1002
 */
1003
function menu_tree_check_access(&$tree, $node_links = array()) {
1004

David_Rothstein's avatar
David_Rothstein committed
1005
  if ($node_links && (user_access('access content') || user_access('bypass node access'))) {
1006 1007
    // Use db_rewrite_sql to evaluate view access without loading each full node.
    $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 1012 1013 1014
      $nid = $node['nid'];
      foreach ($node_links[$nid] as $mlid => $link) {
        $node_links[$nid][$mlid]['access'] = TRUE;
      }
1015 1016
    }
  }
1017
  _menu_tree_check_access($tree);
1018
  return;
1019 1020 1021 1022 1023
}

/**
 * Recursive helper function for menu_tree_check_access()
 */
1024
function _menu_tree_check_access(&$tree) {
1025
  $new_tree = array();
1026 1027
  foreach ($tree as $key => $v) {
    $item = &$tree[$key]['link'];
1028
    _menu_link_translate($item);
1029 1030
    if ($item['access']) {
      if ($tree[$key]['below']) {
1031
        _menu_tree_check_access($tree[$key]['below']);
1032 1033 1034 1035 1036
      }
      // 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];
1037 1038
    }
  }
1039 1040 1041
  // Sort siblings in the tree based on the weights and localized titles.
  ksort($new_tree);
  $tree = $new_tree;
1042
}
1043

1044
/**
1045
 * Build the data representing a menu tree.
1046 1047 1048
 *
 * @param $result
 *   The database result.
1049 1050 1051
 * @param $parents
 *   An array of the plid values that represent the path from the current page
 *   to the root of the menu tree.
1052 1053 1054
 * @param $depth
 *   The depth of the current menu tree.
 * @return
1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067
 *   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.
1068
 */
1069
function _menu_tree_data($result, $parents, $depth, $previous_element = '') {
1070
  $remnant = NULL;
1071
  $tree = array();
1072
  while ($item = db_fetch_array($result)) {
1073 1074
    // We need to determine if we're on the path to root so we can later build
    // 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) {