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

Dries's avatar
 
Dries committed
115
/**
Dries's avatar
 
Dries committed
116
 * @} End of "Menu flags".
Dries's avatar
 
Dries committed
117 118 119
 */

/**
Dries's avatar
 
Dries committed
120
 * @name Menu item types
Dries's avatar
 
Dries committed
121 122 123 124
 * @{
 * Menu item definitions provide one of these constants, which are shortcuts for
 * combinations of the above flags.
 */
Dries's avatar
 
Dries committed
125

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

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

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

/**
154 155 156 157 158
 * 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
159 160 161
 */
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK);

Dries's avatar
 
Dries committed
162
/**
163 164
 * Menu type -- The "default" local task, which is initially active.
 *
Dries's avatar
 
Dries committed
165 166 167 168 169
 * 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
170
/**
Dries's avatar
 
Dries committed
171
 * @} End of "Menu item types".
Dries's avatar
 
Dries committed
172 173 174 175 176
 */

/**
 * @name Menu status codes
 * @{
Dries's avatar
 
Dries committed
177 178
 * Status codes for menu callbacks.
 */
Dries's avatar
 
Dries committed
179

180 181 182
/**
 * Internal menu status code -- Menu item was found.
 */
Dries's avatar
 
Dries committed
183
define('MENU_FOUND', 1);
184 185 186 187

/**
 * Internal menu status code -- Menu item was not found.
 */
Dries's avatar
 
Dries committed
188
define('MENU_NOT_FOUND', 2);
189 190 191 192

/**
 * Internal menu status code -- Menu item access is denied.
 */
Dries's avatar
 
Dries committed
193
define('MENU_ACCESS_DENIED', 3);
194 195 196 197

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

Dries's avatar
 
Dries committed
200
/**
Dries's avatar
 
Dries committed
201
 * @} End of "Menu status codes".
Dries's avatar
 
Dries committed
202
 */
203

204
/**
205
 * @Name Menu tree parameters
206
 * @{
207
 * Menu tree
208 209
 */

210 211
 /**
 * The maximum number of path elements for a menu callback
212
 */
213
define('MENU_MAX_PARTS', 7);
214

215 216

/**
217
 * The maximum depth of a menu links tree - matches the number of p columns.
218
 */
219
define('MENU_MAX_DEPTH', 9);
220

221 222

/**
223
 * @} End of "Menu tree parameters".
224 225
 */

226
/**
227 228 229
 * Returns the ancestors (and relevant placeholders) for any given path.
 *
 * For example, the ancestors of node/12345/edit are:
230 231 232 233 234 235 236
 * - node/12345/edit
 * - node/12345/%
 * - node/%/edit
 * - node/%/%
 * - node/12345
 * - node/%
 * - node
237 238 239 240 241
 *
 * 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
242 243 244
 * 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().
245 246 247 248 249 250
 *
 * @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
251
 *   simply contain as many '%s' as the ancestors.
252 253
 */
function menu_get_ancestors($parts) {
254
  $number_parts = count($parts);
255 256
  $placeholders = array();
  $ancestors = array();
257 258 259 260 261 262 263 264 265 266 267 268 269
  $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;
    }
270 271 272 273 274 275 276 277 278 279 280
    $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
281
    }
282 283
    $placeholders[] = "'%s'";
    $ancestors[] = $current;
284
  }
285
  return array($ancestors, $placeholders);
Dries's avatar
 
Dries committed
286 287 288
}

/**
289 290 291 292
 * 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
293
 *
294
 * Integer values are mapped according to the $map parameter. For
295 296 297 298 299
 * 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').
300
 *
301 302 303 304
 * @param @data
 *   A serialized array.
 * @param @map
 *   An array of potential replacements.
305
 * @return
306
 *   The $data array unserialized and mapped.
307
 */
308 309 310 311 312 313 314 315
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;
316
  }
317 318
  else {
    return array();
319 320 321
  }
}

322 323


324
/**
325
 * Replaces the statically cached item for a given path.
326
 *
327
 * @param $path
328 329 330 331 332 333
 *   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.
334
 */
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
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) {
355
  static $router_items;
356 357 358
  if (!isset($path)) {
    $path = $_GET['q'];
  }
359 360 361
  if (isset($router_item)) {
    $router_items[$path] = $router_item;
  }
362
  if (!isset($router_items[$path])) {
363
    $original_map = arg(NULL, $path);
364
    $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
365
    list($ancestors, $placeholders) = menu_get_ancestors($parts);
366

367
    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))) {
368
      $map = _menu_translate($router_item, $original_map);
369
      if ($map === FALSE) {
370
        $router_items[$path] = FALSE;
371
        return FALSE;
372
      }
373 374 375
      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
376 377
      }
    }
378
    $router_items[$path] = $router_item;
Dries's avatar
 
Dries committed
379
  }
380
  return $router_items[$path];
Dries's avatar
 
Dries committed
381 382 383
}

/**
384
 * Execute the page callback associated with the current path
Dries's avatar
 
Dries committed
385
 */
386
function menu_execute_active_handler($path = NULL) {
387 388
  if (_menu_site_is_offline()) {
    return MENU_SITE_OFFLINE;
389
  }
390
  if (variable_get('menu_rebuild_needed', FALSE)) {
391 392
    menu_rebuild();
  }
393
  if ($router_item = menu_get_item($path)) {
394
    registry_load_path_files();
395
    if ($router_item['access']) {
396 397
      if (drupal_function_exists($router_item['page_callback'])) {
        return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
398 399 400 401 402
      }
    }
    else {
      return MENU_ACCESS_DENIED;
    }
Dries's avatar
 
Dries committed
403
  }
404 405
  return MENU_NOT_FOUND;
}
Dries's avatar
 
Dries committed
406

407
/**
408
 * Loads objects into the map as defined in the $item['load_functions'].
409
 *
410
 * @param $item
411
 *   A menu router or menu link item
412 413 414
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
415 416
 *   Returns TRUE for success, FALSE if an object cannot be loaded.
 *   Names of object loading functions are placed in $item['load_functions'].
417
 *   Loaded objects are placed in $map[]; keys are the same as keys in the
418 419
 *   $item['load_functions'] array.
 *   $item['access'] is set to FALSE if an object cannot be loaded.
420
 */
421 422 423 424 425 426
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;
    }
427 428 429
    $path_map = $map;
    foreach ($load_functions as $index => $function) {
      if ($function) {
430 431 432 433 434 435 436
        $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);
437
          $load_functions[$index] = $function;
438 439 440

          // Some arguments are placeholders for dynamic items to process.
          foreach ($args as $i => $arg) {
441
            if ($arg === '%index') {
442 443 444 445
              // Pass on argument index to the load function, so multiple
              // occurances of the same placeholder can be identified.
              $args[$i] = $index;
            }
446
            if ($arg === '%map') {
447 448 449 450 451
              // 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;
            }
452 453 454
            if (is_int($arg)) {
              $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : '';
            }
455 456 457 458 459 460 461
          }
          array_unshift($args, $value);
          $return = call_user_func_array($function, $args);
        }
        else {
          $return = $function($value);
        }
462 463
        // If callback returned an error or there is no callback, trigger 404.
        if ($return === FALSE) {
464
          $item['access'] = FALSE;
465
          $map = FALSE;
466
          return FALSE;
467 468 469 470
        }
        $map[$index] = $return;
      }
    }
471
    $item['load_functions'] = $load_functions;
472
  }
473 474 475 476 477 478 479
  return TRUE;
}

/**
 * Check access to a menu item using the access callback
 *
 * @param $item
480
 *   A menu router or menu link item
481 482 483
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
484
 *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
485 486
 */
function _menu_check_access(&$item, $map) {
487 488
  // Determine access callback, which will decide whether or not the current
  // user has access to this path.
489
  $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']);
490
  // Check for a TRUE or FALSE value.
491
  if (is_numeric($callback)) {
492
    $item['access'] = (bool)$callback;
Dries's avatar
 
Dries committed
493
  }
494
  else {
495
    $arguments = menu_unserialize($item['access_arguments'], $map);
496 497 498
    // 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') {
499
      $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
500 501
    }
    else {
502
      $item['access'] = call_user_func_array($callback, $arguments);
503
    }
Dries's avatar
 
Dries committed
504
  }
505
}
506

507
/**
508
 * Localize the router item title using t() or another callback.
509
 *
510 511 512 513 514 515 516 517 518 519 520 521 522
 * 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.
523 524 525
 * @return
 *   No return value.
 *   $item['title'] is localized according to $item['title_callback'].
526
 *   If an item's callback is check_plain(), $item['options']['html'] becomes
527 528
 *   TRUE.
 *   $item['description'] is translated using t().
529
 *   When doing link translation and the $item['options']['attributes']['title']
530
 *   (link title attribute) matches the description, it is translated as well.
531 532
 */
function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
533
  $callback = $item['title_callback'];
534
  $item['localized_options'] = $item['options'];
535 536 537 538 539 540 541 542 543 544 545 546
  // If we are not doing link translation or if the title matches the
  // link title of its router item, localize it.
  if (!$link_translate || (!empty($item['title']) && ($item['title'] == $item['link_title']))) {
    // 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));
      }
547
    }
548 549 550 551 552 553 554
    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));
      }
555 556
      // Avoid calling check_plain again on l() function.
      if ($callback == 'check_plain') {
557
        $item['localized_options']['html'] = TRUE;
558
      }
559 560
    }
  }
561 562
  elseif ($link_translate) {
    $item['title'] = $item['link_title'];
563 564 565
  }

  // Translate description, see the motivation above.
566
  if (!empty($item['description'])) {
567
    $original_description = $item['description'];
568
    $item['description'] = t($item['description']);
569
    if ($link_translate && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) {
570
      $item['localized_options']['attributes']['title'] = $item['description'];
571
    }
572
  }
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589
}

/**
 * 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
 *
590 591
 * @param $router_item
 *   A menu router item
592 593 594
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @param $to_arg
595
 *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
596 597 598
 *   path from the menu table, for example tabs.
 * @return
 *   Returns the map with objects loaded as defined in the
599 600
 *   $item['load_functions. $item['access'] becomes TRUE if the item is
 *   accessible, FALSE otherwise. $item['href'] is set according to the map.
601 602 603
 *   If an error occurs during calling the load_functions (like trying to load
 *   a non existing node) then this function return FALSE.
 */
604
function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
605
  $path_map = $map;
606
  if (!_menu_load_objects($router_item, $map)) {
607
    // An error occurred loading an object.
608
    $router_item['access'] = FALSE;
609 610 611
    return FALSE;
  }
  if ($to_arg) {
612
    _menu_link_map_translate($path_map, $router_item['to_arg_functions']);
613 614 615
  }

  // Generate the link path for the page request or local tasks.
616 617
  $link_map = explode('/', $router_item['path']);
  for ($i = 0; $i < $router_item['number_parts']; $i++) {
618 619 620 621
    if ($link_map[$i] == '%') {
      $link_map[$i] = $path_map[$i];
    }
  }
622
  $router_item['href'] = implode('/', $link_map);
623
  $router_item['options'] = array();
624
  _menu_check_access($router_item, $map);
625

626
  _menu_item_localize($router_item, $map);
627 628 629 630 631 632 633 634 635 636 637 638

  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
639
 *   An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
640 641 642 643 644 645
 */
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.
646
      $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
647 648 649 650 651 652 653 654 655 656
      if (!empty($map[$index]) || isset($arg)) {
        $map[$index] = $arg;
      }
      else {
        unset($map[$index]);
      }
    }
  }
}

657 658 659 660
function menu_tail_to_arg($arg, $map, $index) {
  return implode('/', array_slice($map, $index));
}

661 662 663 664 665
/**
 * This function is similar to _menu_translate() but does link-specific
 * preparation such as always calling to_arg functions
 *
 * @param $item
666
 *   A menu link
667 668
 * @return
 *   Returns the map of path arguments with objects loaded as defined in the
669 670 671 672
 *   $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.
673
 *   $item['options'] is unserialized; it is also changed within the call here
674
 *   to $item['localized_options'] by _menu_item_localize().
675 676
 */
function _menu_link_translate(&$item) {
677
  $item['options'] = unserialize($item['options']);
678 679
  if ($item['external']) {
    $item['access'] = 1;
680
    $map = array();
681 682
    $item['href'] = $item['link_path'];
    $item['title'] = $item['link_title'];
683
    $item['localized_options'] = $item['options'];
684 685
  }
  else {
686 687 688
    $map = explode('/', $item['link_path']);
    _menu_link_map_translate($map, $item['to_arg_functions']);
    $item['href'] = implode('/', $map);
689

690
    // Note - skip callbacks without real values for their arguments.
691 692
    if (strpos($item['href'], '%') !== FALSE) {
      $item['access'] = FALSE;
693 694
      return FALSE;
    }
695
    // menu_tree_check_access() may set this ahead of time for links to nodes.
696 697
    if (!isset($item['access'])) {
      if (!_menu_load_objects($item, $map)) {
698
        // An error occurred loading an object.
699 700 701
        $item['access'] = FALSE;
        return FALSE;
      }
702 703
      _menu_check_access($item, $map);
    }
704

705
    _menu_item_localize($item, $map, TRUE);
706
  }
707

708 709 710 711 712 713
  // 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);
  }
714

715
  return $map;
Dries's avatar
 
Dries committed
716 717
}

718 719 720 721 722
/**
 * 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
723 724 725
 * 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.
726 727 728 729 730 731 732 733 734 735
 *
 * @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
736
 *   See menu_get_item() for more on this. Defaults to the current path.
737 738 739
 */
function menu_get_object($type = 'node', $position = 1, $path = NULL) {
  $router_item = menu_get_item($path);
740
  if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type . '_load') {
741 742 743 744
    return $router_item['map'][$position];
  }
}

745
/**
746 747 748 749 750
 * 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).
751 752 753 754 755 756 757 758 759 760
 *
 * @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])) {
761
    $tree = menu_tree_page_data($menu_name);
762 763 764 765 766
    $menu_output[$menu_name] = menu_tree_output($tree);
  }
  return $menu_output[$menu_name];
}

Dries's avatar
 
Dries committed
767
/**
768
 * Returns a rendered menu tree.
769 770 771 772 773
 *
 * @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
774
 */
775 776
function menu_tree_output($tree) {
  $output = '';
777
  $items = array();
778

779
  // Pull out just the menu items we are going to render so that we
780
  // get an accurate count for the first/last classes.
781
  foreach ($tree as $data) {
782
    if (!$data['link']['hidden']) {
783 784 785
      $items[] = $data;
    }
  }
786

787 788 789 790 791 792 793 794 795 796 797 798 799 800 801
  $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);
802
    }
803 804 805 806
  }
  return $output ? theme('menu_tree', $output) : '';
}

807
/**
808 809 810 811
 * 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.
812 813 814 815 816 817 818 819 820 821
 *
 * @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.
 */
822
function menu_tree_all_data($menu_name = 'navigation', $item = NULL) {
823 824
  static $tree = array();

825
  // Use $mlid as a flag for whether the data being loaded is for the whole tree.
826
  $mlid = isset($item['mlid']) ? $item['mlid'] : 0;
827
  // Generate a cache ID (cid) specific for this $menu_name and $item.
828
  $cid = 'links:' . $menu_name . ':all-cid:' . $mlid;
829 830

  if (!isset($tree[$cid])) {
831
    // If the static variable doesn't have the data, check {cache_menu}.
832 833
    $cache = cache_get($cid, 'cache_menu');
    if ($cache && isset($cache->data)) {
834 835 836 837 838 839
      // 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;
      }
840
    }
841 842
    // If the tree data was not in the cache, $data will be NULL.
    if (!isset($data)) {
843
      // Build and run the query, and build the tree.
844
      if ($mlid) {
845 846
        // 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.
847 848 849 850
        $args = array(0);
        for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
          $args[] = $item["p$i"];
        }
851 852
        $args = array_unique($args);
        $placeholders = implode(', ', array_fill(0, count($args), '%d'));
853
        $where = ' AND ml.plid IN (' . $placeholders . ')';
854 855 856 857
        $parents = $args;
        $parents[] = $item['mlid'];
      }
      else {
858
        // Get all links in this menu.
859 860 861 862 863
        $where = '';
        $args = array();
        $parents = array();
      }
      array_unshift($args, $menu_name);
864
      // Select the links from the table, and recursively build the tree.  We
865
      // LEFT JOIN since there is no match in {menu_router} for an external
866
      // link.
867
      $data['tree'] = menu_tree_data(db_query("
868
        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.*
869
        FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
870
        WHERE ml.menu_name = '%s'" . $where . "
871
        ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC", $args), $parents);
872 873
      $data['node_links'] = array();
      menu_tree_collect_node_links($data['tree'], $data['node_links']);
874 875 876 877 878 879 880
      // 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');
881
    }
882
    // Check access for the current user to each item in the tree.
883
    menu_tree_check_access($data['tree'], $data['node_links']);
884
    $tree[$cid] = $data['tree'];
885 886 887 888 889
  }

  return $tree[$cid];
}

890
/**
891 892 893
 * 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
894 895 896 897 898 899 900 901
 * 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
902 903
 *   submenu below the link if there is one, and it is a subtree that has the
 *   same structure described for the top-level array.
904
 */
905
function menu_tree_page_data($menu_name = 'navigation') {
906 907
  static $tree = array();

908
  // Load the menu item corresponding to the current page.
909
  if ($item = menu_get_item()) {
910
    // Generate a cache ID (cid) specific for this page.
911
    $cid = 'links:' . $menu_name . ':page-cid:' . $item['href'] . ':' . (int)$item['access'];
912

913
    if (!isset($tree[$cid])) {
914
      // If the static variable doesn't have the data, check {cache_menu}.
915 916
      $cache = cache_get($cid, 'cache_menu');
      if ($cache && isset($cache->data)) {
917 918 919 920 921 922
        // 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;
        }
923
      }
924 925
      // If the tree data was not in the cache, $data will be NULL.
      if (!isset($data)) {
926
        // Build and run the query, and build the tree.
927
        if ($item['access']) {
928
          // Check whether a menu link exists that corresponds to the current path.
929 930 931 932 933 934
          $args = array($menu_name, $item['href']);
          $placeholders = "'%s'";
          if (drupal_is_front_page()) {
            $args[] = '<front>';
            $placeholders .= ", '%s'";
          }
935
          $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));
936

937
          if (empty($parents)) {
938
            // If no link exists, we may be on a local task that's not in the links.
939
            // TODO: Handle the case like a local task on a specific node in the menu.
940
            $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']));
941
          }
942
          // We always want all the top-level links with plid == 0.
943 944
          $parents[] = '0';