menu.inc 74.5 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

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);
Dries's avatar
 
Dries committed
87

Dries's avatar
 
Dries committed
88
/**
Dries's avatar
 
Dries committed
89
 * @} End of "Menu flags".
Dries's avatar
 
Dries committed
90 91 92
 */

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

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

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

Dries's avatar
 
Dries committed
112
/**
Dries's avatar
 
Dries committed
113 114
 * 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.
115 116
 * 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
117
 */
118
define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010);
Dries's avatar
 
Dries committed
119 120

/**
Dries's avatar
 
Dries committed
121 122 123
 * 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
124 125 126
 */
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK);

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

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

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

Dries's avatar
 
Dries committed
148
/**
Dries's avatar
 
Dries committed
149
 * @} End of "Menu status codes".
Dries's avatar
 
Dries committed
150
 */
151

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

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

163 164

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

169 170

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

174
/**
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
 * 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
191 192 193
 * 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().
194 195 196 197 198 199
 *
 * @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
200
 *   simply contain as many '%s' as the ancestors.
201 202
 */
function menu_get_ancestors($parts) {
203
  $number_parts = count($parts);
204 205
  $placeholders = array();
  $ancestors = array();
206 207 208 209 210 211 212 213 214 215 216 217 218
  $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;
    }
219 220 221 222 223 224 225 226 227 228 229
    $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
230
    }
231 232
    $placeholders[] = "'%s'";
    $ancestors[] = $current;
233
  }
234
  return array($ancestors, $placeholders);
Dries's avatar
 
Dries committed
235 236 237
}

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

271 272


273
/**
274
 * Replaces the statically cached item for a given path.
275
 *
276
 * @param $path
277 278 279 280 281 282
 *   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.
283
 */
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
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) {
304
  static $router_items;
305 306 307
  if (!isset($path)) {
    $path = $_GET['q'];
  }
308 309 310
  if (isset($router_item)) {
    $router_items[$path] = $router_item;
  }
311
  if (!isset($router_items[$path])) {
312
    $original_map = arg(NULL, $path);
313
    $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
314
    list($ancestors, $placeholders) = menu_get_ancestors($parts);
315

316 317
    if ($router_item = db_fetch_array(db_query_range('SELECT * FROM {menu_router} WHERE path IN ('. implode (',', $placeholders) .') ORDER BY fit DESC', $ancestors, 0, 1))) {
      $map = _menu_translate($router_item, $original_map);
318
      if ($map === FALSE) {
319
        $router_items[$path] = FALSE;
320
        return FALSE;
321
      }
322 323 324
      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
325 326
      }
    }
327
    $router_items[$path] = $router_item;
Dries's avatar
 
Dries committed
328
  }
329
  return $router_items[$path];
Dries's avatar
 
Dries committed
330 331 332
}

/**
333
 * Execute the page callback associated with the current path
Dries's avatar
 
Dries committed
334
 */
335
function menu_execute_active_handler($path = NULL) {
336 337
  if (_menu_site_is_offline()) {
    return MENU_SITE_OFFLINE;
338 339
  }
  if ($router_item = menu_get_item($path)) {
340 341 342
    if ($router_item['access']) {
      if ($router_item['file']) {
        require_once($router_item['file']);
343
      }
344
      return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
345 346 347 348
    }
    else {
      return MENU_ACCESS_DENIED;
    }
Dries's avatar
 
Dries committed
349
  }
350 351
  return MENU_NOT_FOUND;
}
Dries's avatar
 
Dries committed
352

353
/**
354
 * Loads objects into the map as defined in the $item['load_functions'].
355
 *
356
 * @param $item
357
 *   A menu router or menu link item
358 359 360
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
361 362
 *   Returns TRUE for success, FALSE if an object cannot be loaded
 */
363 364 365 366 367 368
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;
    }
369 370 371
    $path_map = $map;
    foreach ($load_functions as $index => $function) {
      if ($function) {
372 373 374 375 376 377 378
        $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);
379
          $load_functions[$index] = $function;
380 381 382 383 384 385 386 387 388 389 390 391 392 393

          // 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;
            }
394 395 396
            if (is_int($arg)) {
              $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : '';
            }
397 398 399 400 401 402 403
          }
          array_unshift($args, $value);
          $return = call_user_func_array($function, $args);
        }
        else {
          $return = $function($value);
        }
404 405
        // If callback returned an error or there is no callback, trigger 404.
        if ($return === FALSE) {
406
          $item['access'] = FALSE;
407
          $map = FALSE;
408
          return FALSE;
409 410 411 412
        }
        $map[$index] = $return;
      }
    }
413
    $item['load_functions'] = $load_functions;
414
  }
415 416 417 418 419 420 421
  return TRUE;
}

/**
 * Check access to a menu item using the access callback
 *
 * @param $item
422
 *   A menu router or menu link item
423 424 425
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
426
 *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
427 428
 */
function _menu_check_access(&$item, $map) {
429 430
  // Determine access callback, which will decide whether or not the current
  // user has access to this path.
431
  $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']);
432
  // Check for a TRUE or FALSE value.
433
  if (is_numeric($callback)) {
434
    $item['access'] = (bool)$callback;
Dries's avatar
 
Dries committed
435
  }
436
  else {
437
    $arguments = menu_unserialize($item['access_arguments'], $map);
438 439 440
    // 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') {
441
      $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
442 443
    }
    else {
444
      $item['access'] = call_user_func_array($callback, $arguments);
445
    }
Dries's avatar
 
Dries committed
446
  }
447
}
448

449 450 451
/**
 * Localize the item title using t() or another callback.
 */
452
function _menu_item_localize(&$item, $map) {
453 454 455
  // 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.
456
  $callback = $item['title_callback'];
457 458 459
  // 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') {
460 461
    if (empty($item['title_arguments'])) {
      $item['title'] = t($item['title']);
462 463
    }
    else {
464
      $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
465 466 467
    }
  }
  else {
468 469
    if (empty($item['title_arguments'])) {
      $item['title'] = $callback($item['title']);
470 471
    }
    else {
472
      $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
473 474 475 476
    }
  }

  // Translate description, see the motivation above.
477 478
  if (!empty($item['description'])) {
    $item['description'] = t($item['description']);
479
  }
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
}

/**
 * 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
 *
497 498
 * @param $router_item
 *   A menu router item
499 500 501
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @param $to_arg
502
 *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
503 504 505
 *   path from the menu table, for example tabs.
 * @return
 *   Returns the map with objects loaded as defined in the
506 507
 *   $item['load_functions. $item['access'] becomes TRUE if the item is
 *   accessible, FALSE otherwise. $item['href'] is set according to the map.
508 509 510
 *   If an error occurs during calling the load_functions (like trying to load
 *   a non existing node) then this function return FALSE.
 */
511
function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
512
  $path_map = $map;
513
  if (!_menu_load_objects($router_item, $map)) {
514
    // An error occurred loading an object.
515
    $router_item['access'] = FALSE;
516 517 518
    return FALSE;
  }
  if ($to_arg) {
519
    _menu_link_map_translate($path_map, $router_item['to_arg_functions']);
520 521 522
  }

  // Generate the link path for the page request or local tasks.
523 524
  $link_map = explode('/', $router_item['path']);
  for ($i = 0; $i < $router_item['number_parts']; $i++) {
525 526 527 528
    if ($link_map[$i] == '%') {
      $link_map[$i] = $path_map[$i];
    }
  }
529
  $router_item['href'] = implode('/', $link_map);
530
  $router_item['options'] = array();
531
  _menu_check_access($router_item, $map);
532

533
  _menu_item_localize($router_item, $map);
534 535 536 537 538 539 540 541 542 543 544 545

  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
546
 *   An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
547 548 549 550 551 552
 */
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.
553
      $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
554 555 556 557 558 559 560 561 562 563
      if (!empty($map[$index]) || isset($arg)) {
        $map[$index] = $arg;
      }
      else {
        unset($map[$index]);
      }
    }
  }
}

564 565 566 567
function menu_tail_to_arg($arg, $map, $index) {
  return implode('/', array_slice($map, $index));
}

568 569 570 571 572
/**
 * This function is similar to _menu_translate() but does link-specific
 * preparation such as always calling to_arg functions
 *
 * @param $item
573
 *   A menu link
574 575
 * @return
 *   Returns the map of path arguments with objects loaded as defined in the
576 577 578 579
 *   $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.
580 581
 */
function _menu_link_translate(&$item) {
582 583
  if ($item['external']) {
    $item['access'] = 1;
584
    $map = array();
585 586
    $item['href'] = $item['link_path'];
    $item['title'] = $item['link_title'];
587 588
  }
  else {
589 590 591
    $map = explode('/', $item['link_path']);
    _menu_link_map_translate($map, $item['to_arg_functions']);
    $item['href'] = implode('/', $map);
592

593
    // Note - skip callbacks without real values for their arguments.
594 595
    if (strpos($item['href'], '%') !== FALSE) {
      $item['access'] = FALSE;
596 597
      return FALSE;
    }
598
    // menu_tree_check_access() may set this ahead of time for links to nodes.
599 600
    if (!isset($item['access'])) {
      if (!_menu_load_objects($item, $map)) {
601
        // An error occurred loading an object.
602 603 604
        $item['access'] = FALSE;
        return FALSE;
      }
605 606 607
      _menu_check_access($item, $map);
    }
    // If the link title matches that of a router item, localize it.
608 609
    if (!empty($item['title']) && (($item['title'] == $item['link_title']) || ($item['title_callback'] != 't'))) {
      _menu_item_localize($item, $map);
610 611 612
    }
    else {
      $item['title'] = $item['link_title'];
613 614
    }
  }
615
  $item['options'] = unserialize($item['options']);
616

617
  return $map;
Dries's avatar
 
Dries committed
618 619
}

620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644
/**
 * 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
 * account on user/5 etc.
 *
 * @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
 *   See @menu_get_item for more on this. Defaults to the current path.
 */
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];
  }
}

645
/**
646 647 648 649 650
 * 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).
651 652 653 654 655 656 657 658 659 660
 *
 * @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])) {
661
    $tree = menu_tree_page_data($menu_name);
662 663 664 665 666
    $menu_output[$menu_name] = menu_tree_output($tree);
  }
  return $menu_output[$menu_name];
}

Dries's avatar
 
Dries committed
667
/**
668
 * Returns a rendered menu tree.
669 670 671 672 673
 *
 * @param $tree
 *   A data structure representing the tree as returned from menu_tree_data.
 * @return
 *   The rendered HTML of that data structure.
Dries's avatar
 
Dries committed
674
 */
675 676
function menu_tree_output($tree) {
  $output = '';
677
  $items = array();
678

679
  // Pull out just the menu items we are going to render so that we
680
  // get an accurate count for the first/last classes.
681
  foreach ($tree as $data) {
682
    if (!$data['link']['hidden']) {
683 684 685
      $items[] = $data;
    }
  }
686

687 688 689 690 691 692 693 694 695 696 697 698 699 700 701
  $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);
702
    }
703 704 705 706
  }
  return $output ? theme('menu_tree', $output) : '';
}

707
/**
708 709 710 711
 * 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.
712 713 714 715 716 717 718 719 720 721
 *
 * @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.
 */
722
function menu_tree_all_data($menu_name = 'navigation', $item = NULL) {
723 724
  static $tree = array();

725
  // Use $mlid as a flag for whether the data being loaded is for the whole tree.
726
  $mlid = isset($item['mlid']) ? $item['mlid'] : 0;
727
  // Generate the cache ID.
728
  $cid = 'links:'. $menu_name .':all:'. $mlid;
729 730

  if (!isset($tree[$cid])) {
731
    // If the static variable doesn't have the data, check {cache_menu}.
732 733
    $cache = cache_get($cid, 'cache_menu');
    if ($cache && isset($cache->data)) {
734
      $data = $cache->data;
735 736
    }
    else {
737
      // Build and run the query, and build the tree.
738
      if ($mlid) {
739 740
        // 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.
741 742 743 744
        $args = array(0);
        for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
          $args[] = $item["p$i"];
        }
745 746 747 748 749 750 751
        $args = array_unique($args);
        $placeholders = implode(', ', array_fill(0, count($args), '%d'));
        $where = ' AND ml.plid IN ('. $placeholders .')';
        $parents = $args;
        $parents[] = $item['mlid'];
      }
      else {
752
        // Get all links in this menu.
753 754 755 756 757
        $where = '';
        $args = array();
        $parents = array();
      }
      array_unshift($args, $menu_name);
758
      // Select the links from the table, and recursively build the tree.  We
759
      // LEFT JOIN since there is no match in {menu_router} for an external
760
      // link.
761 762
      $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.*
763
        FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
764 765
        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);
766 767
      $data['node_links'] = array();
      menu_tree_collect_node_links($data['tree'], $data['node_links']);
768
      // Cache the data.
769
      cache_set($cid, $data, 'cache_menu');
770
    }
771
    // Check access for the current user to each item in the tree.
772
    menu_tree_check_access($data['tree'], $data['node_links']);
773
    $tree[$cid] = $data['tree'];
774 775 776 777 778
  }

  return $tree[$cid];
}

779
/**
780 781 782
 * 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
783 784 785 786 787 788 789 790
 * 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
791 792
 *   submenu below the link if there is one, and it is a subtree that has the
 *   same structure described for the top-level array.
793
 */
794
function menu_tree_page_data($menu_name = 'navigation') {
795 796
  static $tree = array();

797
  // Load the menu item corresponding to the current page.
798
  if ($item = menu_get_item()) {
799
    // Generate the cache ID.
800
    $cid = 'links:'. $menu_name .':page:'. $item['href'] .':'. (int)$item['access'];
801

802
    if (!isset($tree[$cid])) {
803
      // If the static variable doesn't have the data, check {cache_menu}.
804 805
      $cache = cache_get($cid, 'cache_menu');
      if ($cache && isset($cache->data)) {
806
        $data = $cache->data;
807 808
      }
      else {
809
        // Build and run the query, and build the tree.
810
        if ($item['access']) {
811
          // Check whether a menu link exists that corresponds to the current path.
812
          $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']));
813

814
          if (empty($parents)) {
815
            // If no link exists, we may be on a local task that's not in the links.
816
            // TODO: Handle the case like a local task on a specific node in the menu.
817
            $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']));
818
          }
819
          // We always want all the top-level links with plid == 0.
820 821
          $parents[] = '0';

822 823
          // Use array_values() so that the indices are numeric for array_merge().
          $args = $parents = array_unique(array_values($parents));
824 825
          $placeholders = implode(', ', array_fill(0, count($args), '%d'));
          $expanded = variable_get('menu_expanded', array());
826
          // Check whether the current menu has any links set to be expanded.
827
          if (in_array($menu_name, $expanded)) {
828 829
            // Collect all the links set to be expanded, and then add all of
            // their children to the list as well.
830
            do {
831
              $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));
832
              $num_rows = FALSE;
833 834
              while ($item = db_fetch_array($result)) {
                $args[] = $item['mlid'];
835
                $num_rows = TRUE;
836 837
              }
              $placeholders = implode(', ', array_fill(0, count($args), '%d'));
838
            } while ($num_rows);
839 840 841 842
          }
          array_unshift($args, $menu_name);
        }
        else {
843 844
          // Show only the top-level menu items when access is denied.
          $args = array($menu_name, '0');
845 846 847
          $placeholders = '%d';
          $parents = array();
        }
848 849
        // 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
850
        // link.
851 852
        $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.*
853
          FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
854 855
          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);
856 857
        $data['node_links'] = array();
        menu_tree_collect_node_links($data['tree'], $data['node_links']);
858
        // Cache the data.
859
        cache_set($cid, $data, 'cache_menu');
860
      }
861
      // Check access for the current user to each item in the tree.
862 863
      menu_tree_check_access($data['tree'], $data['node_links']);
      $tree[$cid] = $data['tree'];
864
    }
865
    return $tree[$cid];
866
  }
867 868

  return array();
Dries's avatar
 
Dries committed
869 870
}

871 872 873 874 875 876 877 878
/**
 * 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)) {
879
        $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link'];
880 881 882 883
        $tree[$key]['link']['access'] = FALSE;
      }
    }
    if ($tree[$key]['below']) {
884
      menu_tree_collect_node_links($tree[$key]['below'], $node_links);
885 886 887 888
    }
  }
}

889 890 891
/**
 * Check access and perform other dynamic operations for each link in the tree.
 */
892
function menu_tree_check_access(&$tree, $node_links = array()) {
893 894 895 896

  if ($node_links) {
    // Use db_rewrite_sql to evaluate view access without loading each full node.
    $nids = array_keys($node_links);
897
    $placeholders = '%d'. str_repeat(', %d', count($nids) - 1);
898 899
    $result = db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.nid IN (". $placeholders .")"), $nids);
    while ($node = db_fetch_array($result)) {
900 901 902 903
      $nid = $node['nid'];
      foreach ($node_links[$nid] as $mlid => $link) {
        $node_links[$nid][$mlid]['access'] = TRUE;
      }
904 905
    }
  }
906
  _menu_tree_check_access($tree);
907
  return;
908 909 910 911 912
}

/**
 * Recursive helper function for menu_tree_check_access()
 */
913
function _menu_tree_check_access(&$tree) {
914
  $new_tree = array();
915 916
  foreach ($tree as $key => $v) {
    $item = &$tree[$key]['link'];
917
    _menu_link_translate($item);
918 919
    if ($item['access']) {
      if ($tree[$key]['below']) {
920
        _menu_tree_check_access($tree[$key]['below']);
921 922 923 924 925
      }
      // 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];
926 927
    }
  }
928 929 930
  // Sort siblings in the tree based on the weights and localized titles.
  ksort($new_tree);
  $tree = $new_tree;
931
}
932

933
/**
934
 * Build the data representing a menu tree.
935 936 937
 *
 * @param $result
 *   The database result.
938 939 940
 * @param $parents
 *   An array of the plid values that represent the path from the current page
 *   to the root of the menu tree.
941 942 943
 * @param $depth
 *   The depth of the current menu tree.
 * @return
944 945 946 947 948 949 950 951 952 953 954 955 956
 *   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.
957
 */
958
function _menu_tree_data($result, $parents, $depth, $previous_element = '') {
959
  $remnant = NULL;
960
  $tree = array();
961
  while ($item = db_fetch_array($result)) {
962 963
    // We need to determine if we're on the path to root so we can later build
    // the correct active trail and breadcrumb.
964
    $item['in_active_trail'] = in_array($item['mlid'], $parents);
965
    // The current item is the first in a new submenu.
966
    if ($item['depth'] > $depth) {
967
      // _menu_tree returns an item and the menu tree structure.
968
      list($item, $below) = _menu_tree_data($result, $parents, $item['depth'], $item);
969 970 971 972 973 974 975 976 977
      if ($previous_element) {
        $tree[$previous_element['mlid']] = array(
          'link' => $previous_element,
          'below' => $below,
        );
      }
      else {
        $tree = $below;
      }
978
      // We need to fall back one level.
979
      if (!isset($item) || $item['depth'] < $depth) {
980 981
        return array($item, $tree);
      }
982
      // This will be the link to be output in the next iteration.
983
      $previous_element = $item;
Dries's avatar
 
Dries committed
984
    }
985
    // We are at the same depth, so we use the previous element.
986
    elseif ($item['depth'] == $depth) {
987 988
      if ($previous_element) {
        // Only the first time.
989
        $tree[$previous_element['mlid']] = array(
990
          'link' => $previous_element,
991
          'below' => FALSE,
992 993
        );
      }
994
      // This will be the link to be output in the next iteration.
995
      $previous_element = $item;
Dries's avatar
 
Dries committed
996
    }
997
    // The submenu ended with the previous item, so pass back the current item.
998
    else {
999
      $remnant = $item;
1000
      break;
Dries's avatar
 
Dries committed
1001
    }
Dries's avatar
 
Dries committed
1002
  }
1003
  if ($previous_element) {
1004
    // We have one more link dangling.
1005
    $tree[$previous_element['mlid']] = array(
1006
      'link' => $previous_element,
1007
      'below' => FALSE,
1008
    );
1009 1010
  }
  return array($remnant, $tree);
Dries's avatar
 
Dries committed
1011 1012
}

Dries's avatar
 
Dries committed
1013
/**
1014
 * Generate the HTML output for a single menu link.
1015 1016
 *
 * @ingroup themeable
Dries's avatar
 
Dries committed
1017
 */
1018
function theme_menu_item_link($link) {
1019 1020 1021 1022
  if (empty($link['options'])) {
    $link['options'] = array();
  }

1023
  return l($link['title'], $link['href'], $link['options']);
Dries's avatar
 
Dries committed
1024 1025
}

Dries's avatar
 
Dries committed
1026
/**
1027
 * Generate the HTML output for a menu tree
1028 1029
 *
 * @ingroup themeable
Dries's avatar
 
Dries committed
1030
 */
1031 1032 1033 1034 1035 1036
function theme_menu_tree($tree) {
  return '<ul class="menu">'. $tree .'</ul>';
}

/**
 * Generate the HTML output for a menu item and submenu.
1037 1038
 *
 * @ingroup themeable
1039
 */
1040
function theme_menu_item($link, $has_children, $menu = '', $in_active_trail = FALSE, $extra_class = NULL) {
1041
  $class = ($menu ? 'expanded' : ($has_children ? 'collapsed' : 'leaf'));
1042 1043 1044
  if (!empty($extra_class)) {
    $class .= ' '. $extra_class;
  }
1045 1046 1047 1048
  if ($in_active_trail) {
    $class .= ' active-trail';
  }
  return '<li class="'. $class .'">'. $link . $menu .'</li>'."\n";
1049 1050
}

1051 1052
/**
 * Generate the HTML output for a single local task link.
1053 1054
 *
 * @ingroup themeable
1055
 */
1056
function theme_menu_local_task($link, $active = FALSE) {
1057
  return '<li '. ($active ? 'class="active" ' : '') .'>'. $link .'</li>';
1058
}
Dries's avatar
 
Dries committed
1059

1060 1061 1062 1063 1064 1065 1066 1067
/**
 * 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('', '', '', '', '', '', '', '', '', '', '', '');
}

Dries's avatar
 
Dries committed