menu.inc 110 KB
Newer Older
Dries's avatar
 
Dries committed
1
<?php
2
// $Id$
Kjartan's avatar
Kjartan committed
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
 * @{
Dries's avatar
 
Dries committed
12
 * Define the navigation menus, and route page requests to code based on URLs.
Dries's avatar
 
Dries committed
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.
 *
Steven Wittens's avatar
Steven Wittens committed
40
 * The found callback function is called with any arguments specified
41
 * in the "page arguments" attribute of its menu item. The
Steven Wittens's avatar
Steven Wittens committed
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.
Dries's avatar
 
Dries committed
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.
Dries's avatar
 
Dries committed
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
/**
Dries's avatar
 
Dries committed
75
 * @name Menu flags
Dries's avatar
 
Dries committed
76
 * @{
Dries's avatar
 
Dries committed
77 78
 * Flags for use in the "type" attribute of menu items.
 */
Dries's avatar
 
Dries committed
79

80 81 82
/**
 * Internal menu flag -- menu item is the root of the menu tree.
 */
Dries's avatar
 
Dries committed
83
define('MENU_IS_ROOT', 0x0001);
84 85 86 87

/**
 * Internal menu flag -- menu item is visible in the menu tree.
 */
Dries's avatar
 
Dries committed
88
define('MENU_VISIBLE_IN_TREE', 0x0002);
89 90 91 92

/**
 * Internal menu flag -- menu item is visible in the breadcrumb.
 */
Dries's avatar
 
Dries committed
93
define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004);
94 95 96 97

/**
 * Internal menu flag -- menu item links back to its parnet.
 */
98
define('MENU_LINKS_TO_PARENT', 0x0008);
99 100 101 102

/**
 * Internal menu flag -- menu item can be modified by administrator.
 */
103
define('MENU_MODIFIED_BY_ADMIN', 0x0020);
104 105 106 107

/**
 * Internal menu flag -- menu item was created by administrator.
 */
108
define('MENU_CREATED_BY_ADMIN', 0x0040);
109 110 111 112

/**
 * Internal menu flag -- menu item is a local task.
 */
113
define('MENU_IS_LOCAL_TASK', 0x0080);
Dries's avatar
 
Dries committed
114

115 116 117 118 119
/**
 * Internal menu flag -- menu item is a local action.
 */
define('MENU_IS_LOCAL_ACTION', 0x0100);

Dries's avatar
 
Dries committed
120
/**
Dries's avatar
 
Dries committed
121
 * @} End of "Menu flags".
Dries's avatar
 
Dries committed
122 123 124
 */

/**
Dries's avatar
 
Dries committed
125
 * @name Menu item types
Dries's avatar
 
Dries committed
126 127 128 129
 * @{
 * Menu item definitions provide one of these constants, which are shortcuts for
 * combinations of the above flags.
 */
Dries's avatar
 
Dries committed
130

Dries's avatar
 
Dries committed
131
/**
132 133
 * Menu type -- A "normal" menu item that's shown in menu and breadcrumbs.
 *
Dries's avatar
 
Dries committed
134
 * Normal menu items show up in the menu tree and can be moved/hidden by
Dries's avatar
 
Dries committed
135 136
 * 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
137
 */
138
define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);
139

Dries's avatar
 
Dries committed
140
/**
141 142
 * Menu type -- A hidden, internal callback, typically used for API calls.
 *
Dries's avatar
 
Dries committed
143
 * Callbacks simply register a path so that the correct function is fired
Dries's avatar
 
Dries committed
144
 * when the URL is accessed. They are not shown in the menu.
Dries's avatar
 
Dries committed
145 146
 */
define('MENU_CALLBACK', MENU_VISIBLE_IN_BREADCRUMB);
Dries's avatar
 
Dries committed
147

Dries's avatar
 
Dries committed
148
/**
149 150
 * Menu type -- A normal menu item, hidden until enabled by an administrator.
 *
Dries's avatar
 
Dries committed
151 152
 * 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.
153 154
 * 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
155
 */
156
define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010);
Dries's avatar
 
Dries committed
157 158

/**
159 160 161 162 163
 * Menu type -- A task specific to the parent item, usually rendered as a tab.
 *
 * Local tasks are 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
164 165 166
 */
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK);

Dries's avatar
 
Dries committed
167
/**
168 169
 * Menu type -- The "default" local task, which is initially active.
 *
Dries's avatar
 
Dries committed
170 171 172 173 174
 * 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);

175 176 177 178 179 180 181 182
/**
 * Menu type -- An action specific to the parent, usually rendered as a link.
 *
 * Local actions are menu items that describe actions on the parent item such
 * as adding a new user, taxonomy term, etc.
 */
define('MENU_LOCAL_ACTION', MENU_IS_LOCAL_TASK | MENU_IS_LOCAL_ACTION);

Dries's avatar
 
Dries committed
183
/**
Dries's avatar
 
Dries committed
184
 * @} End of "Menu item types".
Dries's avatar
 
Dries committed
185 186
 */

187 188 189 190 191 192
/**
 * @name Menu context types
 * @{
 * Flags for use in the "context" attribute of menu router items.
 */

193 194 195 196 197 198 199 200
/**
 * Internal menu flag: Invisible local task.
 *
 * This flag may be used for local tasks like "Delete", so custom modules and
 * themes can alter the default context and expose the task by altering menu.
 */
define('MENU_CONTEXT_NONE', 0x0000);

201 202 203 204 205 206 207 208 209 210 211 212 213 214
/**
 * Internal menu flag: Local task should be displayed in page context.
 */
define('MENU_CONTEXT_PAGE', 0x0001);

/**
 * Internal menu flag: Local task should be displayed inline.
 */
define('MENU_CONTEXT_INLINE', 0x0002);

/**
 * @} End of "Menu context types".
 */

Dries's avatar
 
Dries committed
215 216 217
/**
 * @name Menu status codes
 * @{
Dries's avatar
 
Dries committed
218 219
 * Status codes for menu callbacks.
 */
Dries's avatar
 
Dries committed
220

221 222 223
/**
 * Internal menu status code -- Menu item was found.
 */
Dries's avatar
 
Dries committed
224
define('MENU_FOUND', 1);
225 226 227 228

/**
 * Internal menu status code -- Menu item was not found.
 */
Dries's avatar
 
Dries committed
229
define('MENU_NOT_FOUND', 2);
230 231 232 233

/**
 * Internal menu status code -- Menu item access is denied.
 */
Dries's avatar
 
Dries committed
234
define('MENU_ACCESS_DENIED', 3);
235 236 237 238

/**
 * Internal menu status code -- Menu item inaccessible because site is offline.
 */
239
define('MENU_SITE_OFFLINE', 4);
Dries's avatar
 
Dries committed
240

Dries's avatar
 
Dries committed
241
/**
Dries's avatar
 
Dries committed
242
 * @} End of "Menu status codes".
Dries's avatar
 
Dries committed
243
 */
244

245
/**
246
 * @Name Menu tree parameters
247
 * @{
248
 * Menu tree
249 250
 */

251 252
 /**
 * The maximum number of path elements for a menu callback
253
 */
254
define('MENU_MAX_PARTS', 9);
255

256 257

/**
258
 * The maximum depth of a menu links tree - matches the number of p columns.
259
 */
260
define('MENU_MAX_DEPTH', 9);
261

262 263

/**
264
 * @} End of "Menu tree parameters".
265 266
 */

267
/**
268 269 270
 * Returns the ancestors (and relevant placeholders) for any given path.
 *
 * For example, the ancestors of node/12345/edit are:
271 272 273 274 275 276 277
 * - node/12345/edit
 * - node/12345/%
 * - node/%/edit
 * - node/%/%
 * - node/12345
 * - node/%
 * - node
278 279 280 281 282
 *
 * 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
283
 * any argument matches that part. We limit ourselves to using binary
284
 * numbers that correspond the patterns of wildcards of router items that
285
 * actually exists. This list of 'masks' is built in menu_rebuild().
286 287 288 289 290 291
 *
 * @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
292
 *   simply contain as many '%s' as the ancestors.
293 294
 */
function menu_get_ancestors($parts) {
295
  $number_parts = count($parts);
296
  $ancestors = array();
297 298 299 300 301 302 303 304 305 306 307 308 309
  $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;
    }
310 311 312 313 314 315 316 317 318 319 320
    $current = '';
    for ($j = $length; $j >= 0; $j--) {
      if ($i & (1 << $j)) {
        $current .= $parts[$length - $j];
      }
      else {
        $current .= '%';
      }
      if ($j) {
        $current .= '/';
      }
Dries's avatar
 
Dries committed
321
    }
322
    $ancestors[] = $current;
323
  }
324
  return $ancestors;
Dries's avatar
 
Dries committed
325 326 327
}

/**
328 329 330 331
 * 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.
Dries's avatar
 
Dries committed
332
 *
333
 * Integer values are mapped according to the $map parameter. For
334 335 336 337 338
 * 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').
339
 *
340 341 342 343
 * @param @data
 *   A serialized array.
 * @param @map
 *   An array of potential replacements.
344
 * @return
345
 *   The $data array unserialized and mapped.
346
 */
347 348 349 350 351 352 353 354
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;
355
  }
356 357
  else {
    return array();
358 359 360
  }
}

361 362


363
/**
364
 * Replaces the statically cached item for a given path.
365
 *
366
 * @param $path
367 368 369 370 371 372
 *   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.
373
 */
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389
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.
390 391 392
 *   The values for key title, page_arguments, access_arguments, and
 *   theme_arguments will be filled in based on the database values and the
 *   objects loaded.
393 394
 */
function menu_get_item($path = NULL, $router_item = NULL) {
395
  $router_items = &drupal_static(__FUNCTION__);
396 397 398
  if (!isset($path)) {
    $path = $_GET['q'];
  }
399 400 401
  if (isset($router_item)) {
    $router_items[$path] = $router_item;
  }
402
  if (!isset($router_items[$path])) {
403
    $original_map = arg(NULL, $path);
404
    $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
405 406 407 408 409 410
    $ancestors = menu_get_ancestors($parts);
    $router_item = db_select('menu_router')
      ->fields('menu_router')
      ->condition('path', $ancestors, 'IN')
      ->orderBy('fit', 'DESC')
      ->range(0, 1)
411
      ->addTag('menu_get_item')
412 413
      ->execute()->fetchAssoc();
    if ($router_item) {
414
      $map = _menu_translate($router_item, $original_map);
415
      $router_item['original_map'] = $original_map;
416
      if ($map === FALSE) {
417
        $router_items[$path] = FALSE;
418
        return FALSE;
419
      }
420 421 422
      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']));
423
        $router_item['theme_arguments'] = array_merge(menu_unserialize($router_item['theme_arguments'], $map), array_slice($map, $router_item['number_parts']));
Dries's avatar
 
Dries committed
424 425
      }
    }
426
    $router_items[$path] = $router_item;
Dries's avatar
 
Dries committed
427
  }
428
  return $router_items[$path];
Dries's avatar
 
Dries committed
429 430 431
}

/**
432 433 434 435 436 437 438 439 440 441 442
 * Execute the page callback associated with the current path.
 * 
 * @param $path
 *   The drupal path whose handler is to be be executed. If set to NULL, then
 *   the current path is used.
 * @param $deliver
 *   (optional) A boolean to indicate whether the content should be sent to the
 *   browser using the appropriate delivery callback (TRUE) or whether to return
 *   the result to the caller (FALSE).
 */
function menu_execute_active_handler($path = NULL, $deliver = TRUE) {
443
  if (_menu_site_is_offline()) {
444
    $page_callback_result = MENU_SITE_OFFLINE;
445
  }
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465
  else {
    // 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())) {
      menu_rebuild();
    }
    if ($router_item = menu_get_item($path)) {
      // hook_menu_alter() lets modules control menu router information that
      // doesn't depend on the details of a particular page request.
      // Here, we want to give modules a chance to use request-time information
      // to make alterations just for this request.
      drupal_alter('menu_active_handler', $router_item, $path);
      if ($router_item['access']) {
        if ($router_item['file']) {
          require_once DRUPAL_ROOT . '/' . $router_item['file'];
        }
        $page_callback_result = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
      }
      else {
        $page_callback_result = MENU_ACCESS_DENIED;
466 467 468
      }
    }
    else {
469
      $page_callback_result = MENU_NOT_FOUND;
470
    }
Dries's avatar
 
Dries committed
471
  }
472 473 474 475 476 477 478 479 480 481

  // Deliver the result of the page callback to the browser, or if requested,
  // return it raw, so calling code can do more processing.
  if ($deliver) {
    $default_delivery_callback = (isset($router_item) && $router_item) ? $router_item['delivery_callback'] : NULL;
    drupal_deliver_page($page_callback_result, $default_delivery_callback);
  }
  else {
    return $page_callback_result;
  }
482
}
Dries's avatar
 
Dries committed
483

484
/**
485
 * Loads objects into the map as defined in the $item['load_functions'].
486
 *
487
 * @param $item
488
 *   A menu router or menu link item
489 490 491
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
492 493
 *   Returns TRUE for success, FALSE if an object cannot be loaded.
 *   Names of object loading functions are placed in $item['load_functions'].
494
 *   Loaded objects are placed in $map[]; keys are the same as keys in the
495 496
 *   $item['load_functions'] array.
 *   $item['access'] is set to FALSE if an object cannot be loaded.
497
 */
498 499 500 501 502 503
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;
    }
504 505 506
    $path_map = $map;
    foreach ($load_functions as $index => $function) {
      if ($function) {
507 508 509 510 511 512 513
        $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);
514
          $load_functions[$index] = $function;
515 516 517

          // Some arguments are placeholders for dynamic items to process.
          foreach ($args as $i => $arg) {
518
            if ($arg === '%index') {
519
              // Pass on argument index to the load function, so multiple
520
              // occurrences of the same placeholder can be identified.
521 522
              $args[$i] = $index;
            }
523
            if ($arg === '%map') {
524 525 526 527 528
              // 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;
            }
529 530 531
            if (is_int($arg)) {
              $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : '';
            }
532 533 534 535 536 537 538
          }
          array_unshift($args, $value);
          $return = call_user_func_array($function, $args);
        }
        else {
          $return = $function($value);
        }
539 540
        // If callback returned an error or there is no callback, trigger 404.
        if ($return === FALSE) {
541
          $item['access'] = FALSE;
542
          $map = FALSE;
543
          return FALSE;
544 545 546 547
        }
        $map[$index] = $return;
      }
    }
548
    $item['load_functions'] = $load_functions;
549
  }
550 551 552 553 554 555 556
  return TRUE;
}

/**
 * Check access to a menu item using the access callback
 *
 * @param $item
557
 *   A menu router or menu link item
558 559 560
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
561
 *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
562 563
 */
function _menu_check_access(&$item, $map) {
564 565
  // Determine access callback, which will decide whether or not the current
  // user has access to this path.
566
  $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']);
567
  // Check for a TRUE or FALSE value.
568
  if (is_numeric($callback)) {
569
    $item['access'] = (bool)$callback;
Dries's avatar
 
Dries committed
570
  }
571
  else {
572
    $arguments = menu_unserialize($item['access_arguments'], $map);
573 574 575
    // 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') {
576
      $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
577
    }
578
    elseif (function_exists($callback)) {
579
      $item['access'] = call_user_func_array($callback, $arguments);
580
    }
Dries's avatar
 
Dries committed
581
  }
582
}
583

584
/**
585
 * Localize the router item title using t() or another callback.
586
 *
587 588 589 590 591 592 593 594 595 596 597 598 599
 * 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.
600 601 602
 * @return
 *   No return value.
 *   $item['title'] is localized according to $item['title_callback'].
603
 *   If an item's callback is check_plain(), $item['options']['html'] becomes
604 605
 *   TRUE.
 *   $item['description'] is translated using t().
606
 *   When doing link translation and the $item['options']['attributes']['title']
607
 *   (link title attribute) matches the description, it is translated as well.
608 609
 */
function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
610
  $callback = $item['title_callback'];
611
  $item['localized_options'] = $item['options'];
612 613 614 615 616 617 618 619
  // All 'class' attributes are assumed to be an array during rendering, but
  // links stored in the database may use an old string value.
  // @todo In order to remove this code we need to implement a database update
  //   including unserializing all existing link options and running this code
  //   on them, as well as adding validation to menu_link_save().
  if (isset($item['options']['attributes']['class']) && is_string($item['options']['attributes']['class'])) {
    $item['localized_options']['attributes']['class'] = explode(' ', $item['options']['attributes']['class']);
  }
620 621 622 623 624 625 626
  // 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'])) {
627 628 629 630 631 632 633 634 635
    // 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));
      }
636
    }
637
    elseif ($callback && function_exists($callback)) {
638 639 640 641 642 643
      if (empty($item['title_arguments'])) {
        $item['title'] = $callback($item['title']);
      }
      else {
        $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
      }
644 645
      // Avoid calling check_plain again on l() function.
      if ($callback == 'check_plain') {
646
        $item['localized_options']['html'] = TRUE;
647
      }
648 649
    }
  }
650 651
  elseif ($link_translate) {
    $item['title'] = $item['link_title'];
652 653 654
  }

  // Translate description, see the motivation above.
655
  if (!empty($item['description'])) {
656
    $original_description = $item['description'];
657
    $item['description'] = t($item['description']);
658
    if ($link_translate && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) {
659
      $item['localized_options']['attributes']['title'] = $item['description'];
660
    }
661
  }
662 663 664 665 666 667 668 669 670 671 672 673 674 675 676
}

/**
 * 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
677
 * to the language required to generate the current page.
678
 *
679 680
 * @param $router_item
 *   A menu router item
681 682 683
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @param $to_arg
684
 *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
685 686 687
 *   path from the menu table, for example tabs.
 * @return
 *   Returns the map with objects loaded as defined in the
688
 *   $item['load_functions']. $item['access'] becomes TRUE if the item is
689
 *   accessible, FALSE otherwise. $item['href'] is set according to the map.
690 691 692
 *   If an error occurs during calling the load_functions (like trying to load
 *   a non existing node) then this function return FALSE.
 */
693
function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
694
  if ($to_arg && !empty($router_item['to_arg_functions'])) {
695 696 697 698 699
    // 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.
700
  $path_map = $map;
701
  if (!empty($router_item['load_functions']) && !_menu_load_objects($router_item, $map)) {
702
    // An error occurred loading an object.
703
    $router_item['access'] = FALSE;
704 705 706 707
    return FALSE;
  }

  // Generate the link path for the page request or local tasks.
708 709
  $link_map = explode('/', $router_item['path']);
  for ($i = 0; $i < $router_item['number_parts']; $i++) {
710 711 712 713
    if ($link_map[$i] == '%') {
      $link_map[$i] = $path_map[$i];
    }
  }
714
  $router_item['href'] = implode('/', $link_map);
715
  $router_item['options'] = array();
716
  _menu_check_access($router_item, $map);
717

718 719 720 721
  // For performance, don't localize an item the user can't access.
  if ($router_item['access']) {
    _menu_item_localize($router_item, $map);
  }
722 723 724 725 726 727 728 729 730

  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.
 *
731
 * @param $map
732 733
 *   An array of path arguments (ex: array('node', '5'))
 * @param $to_arg_functions
734
 *   An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
735 736
 */
function _menu_link_map_translate(&$map, $to_arg_functions) {
737 738 739 740 741 742 743 744 745
  $to_arg_functions = unserialize($to_arg_functions);
  foreach ($to_arg_functions as $index => $function) {
    // Translate place-holders into real values.
    $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
    if (!empty($map[$index]) || isset($arg)) {
      $map[$index] = $arg;
    }
    else {
      unset($map[$index]);
746 747 748 749
    }
  }
}

750 751 752 753
function menu_tail_to_arg($arg, $map, $index) {
  return implode('/', array_slice($map, $index));
}

754 755 756 757 758
/**
 * This function is similar to _menu_translate() but does link-specific
 * preparation such as always calling to_arg functions
 *
 * @param $item
759
 *   A menu link
760 761
 * @return
 *   Returns the map of path arguments with objects loaded as defined in the
762 763 764 765
 *   $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.
766
 *   $item['options'] is unserialized; it is also changed within the call here
767
 *   to $item['localized_options'] by _menu_item_localize().
768 769
 */
function _menu_link_translate(&$item) {
770
  $item['options'] = unserialize($item['options']);
771 772
  if ($item['external']) {
    $item['access'] = 1;
773
    $map = array();
774 775
    $item['href'] = $item['link_path'];
    $item['title'] = $item['link_title'];
776
    $item['localized_options'] = $item['options'];
777 778
  }
  else {
779
    $map = explode('/', $item['link_path']);
780 781 782
    if (!empty($item['to_arg_functions'])) {
      _menu_link_map_translate($map, $item['to_arg_functions']);
    }
783
    $item['href'] = implode('/', $map);
784

785
    // Note - skip callbacks without real values for their arguments.
786 787
    if (strpos($item['href'], '%') !== FALSE) {
      $item['access'] = FALSE;
788 789
      return FALSE;
    }
790
    // menu_tree_check_access() may set this ahead of time for links to nodes.
791
    if (!isset($item['access'])) {
792
      if (!empty($item['load_functions']) && !_menu_load_objects($item, $map)) {
793
        // An error occurred loading an object.
794 795 796
        $item['access'] = FALSE;
        return FALSE;
      }
797 798
      _menu_check_access($item, $map);
    }
799 800 801 802
    // For performance, don't localize a link the user can't access.
    if ($item['access']) {
      _menu_item_localize($item, $map, TRUE);
    }
803
  }
804

805 806 807 808 809 810
  // 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);
  }
811

812
  return $map;
Dries's avatar
 
Dries committed
813 814
}

815 816 817
/**
 * Get a loaded object from a router item.
 *
818 819 820 821 822 823 824 825 826 827
 * menu_get_object() provides access to objects loaded by the current router
 * item. For example, on the page node/%node, the router loads the %node object,
 * and calling menu_get_object() will return that. Normally, it is necessary to
 * specify the type of object referenced, however node is the default.
 * The following example tests to see whether the node being displayed is of the
 * "story" content type:
 * @code
 * $node = menu_get_object();
 * $story = $node->type == 'story';
 * @endcode
828 829
 *
 * @param $type
830
 *   Type of the object. These appear in hook_menu definitions as %type. Core
831 832 833 834
 *   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
835 836 837
 *   The position of the object in the path, where the first path segment is 0.
 *   For node/%node, the position of %node is 1, but for comment/reply/%node,
 *   it's 2. Defaults to 1.
838
 * @param $path
839
 *   See menu_get_item() for more on this. Defaults to the current path.
840 841 842
 */
function menu_get_object($type = 'node', $position = 1, $path = NULL) {
  $router_item = menu_get_item($path);
843
  if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type . '_load') {
844 845 846 847
    return $router_item['map'][$position];
  }
}

848
/**
849 850 851 852 853
 * 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).
854 855 856 857 858 859
 *
 * @param $menu_name
 *   The name of the menu.
 * @return
 *   The rendered HTML of that menu on the current page.
 */
860
function menu_tree($menu_name) {
861
  $menu_output = &drupal_static(__FUNCTION__, array());
862 863

  if (!isset($menu_output[$menu_name])) {
864
    $tree = menu_tree_page_data($menu_name);
865 866 867 868 869
    $menu_output[$menu_name] = menu_tree_output($tree);
  }
  return $menu_output[$menu_name];
}

Dries's avatar
 
Dries committed
870
/**
871
 * Returns a rendered menu tree.
872
 *
873 874 875 876 877
 * The menu item's LI element is given one of the following classes:
 * - expanded: The menu item is showing its submenu.
 * - collapsed: The menu item has a submenu which is not shown.
 * - leaf: The menu item has no submenu.
 *
878 879 880
 * @param $tree
 *   A data structure representing the tree as returned from menu_tree_data.
 * @return
881
 *   A structured array to be rendered by drupal_render().
Dries's avatar
 
Dries committed
882
 */
883
function menu_tree_output($tree) {
884
  $build = array();
885
  $items = array();
886

887
  // Pull out just the menu links we are going to render so that we
888
  // get an accurate count for the first/last classes.
889
  foreach ($tree as $data) {
890
    if (!$data['link']['hidden']) {
891 892 893
      $items[] = $data;
    }
  }
894

895 896
  $num_items = count($items);
  foreach ($items as $i => $data) {
897
    $class = array();
898
    if ($i == 0) {
899
      $class[] = 'first';
900 901
    }
    if ($i == $num_items - 1) {
902
      $class[] = 'last';
903
    }
904
    // Set a class if the link has children.
905
    if ($data['below']) {
906 907 908 909
      $class[] = 'expanded';
    }
    elseif ($data['link']['has_children']) {
      $class[] = 'collapsed';
910 911
    }
    else {
912
      $class[] = 'leaf';
913
    }
914 915 916 917 918 919 920 921 922 923 924 925 926 927 928
    // Set a class if the link is in the active trail.
    if ($data['link']['in_active_trail']) {
      $class[] = 'active-trail';
      $data['localized_options']['attributes']['class'][] = 'active-trail';
    }

    $element['#theme'] = 'menu_link';
    $element['#attributes']['class'] = $class;
    $element['#title'] = $data['link']['title'];
    $element['#href'] = $data['link']['href'];
    $element['#localized_options'] = !empty($data['localized_options']) ? $data['localized_options'] : array();
    $element['#below'] = $data['below'] ? menu_tree_output($data['below']) : $data['below'];
    $element['#original_link'] = $data['link'];
    // Index using the link's unique mlid.
    $build[$data['link']['mlid']] = $element;
929
  }
930 931 932 933 934 935 936 937
  if ($build) {
    // Make sure drupal_render() does not re-order the links.
    $build['#sorted'] = TRUE;
    // Add the theme wrapper for outer markup.
    $build['#theme_wrappers'][] = 'menu_tree';
  }

  return $build;
938 939
}

940
/**
941 942 943 944
 * 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.
945 946 947
 *
 * @param $menu_name
 *   The named menu links to return
948
 * @param $link
949
 *   A fully loaded menu link, or NULL. If a link is supplied, only the
950
 *   path to root will be included in the returned tree - as if this link
951
 *   represented the current page in a visible menu.
952 953 954 955 956
 * @param $max_depth
 *   Optional maximum depth of links to retrieve. Typically useful if only one
 *   or two levels of a sub tree are needed in conjunction with a non-NULL
 *   $link, in which case $max_depth should be greater than $link['depth'].
 *
957 958 959
 * @return
 *   An tree of menu links in an array, in the order they should be rendered.
 */
960
function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) {
961
  $tree = &drupal_static(__FUNCTION__, array());
962

963
  // Use $mlid as a flag for whether the data being loaded is for the whole tree.
964
  $mlid = isset($link['mlid']) ? $link['mlid'] : 0;
965 966
  // Generate a cache ID (cid) specific for this $menu_name, $link, $language, and depth.
  $cid = 'links:' . $menu_name . ':all-cid:' . $mlid . ':' . $GLOBALS['language_interface']->language . ':' . (int)$max_depth;
967 968

  if (!isset($tree[$cid])) {
969
    // If the static variable doesn't have the data, check {cache_menu}.
970 971
    $cache = cache_get($cid, 'cache_menu');
    if ($cache && isset($cache->data)) {
Dries's avatar