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

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

Dries's avatar
 
Dries committed
8 9 10
/**
 * @defgroup menu Menu system
 * @{
Dries's avatar
 
Dries committed
11
 * Define the navigation menus, and route page requests to code based on URLs.
Dries's avatar
 
Dries committed
12 13 14 15
 *
 * 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
16 17 18 19
 * menu system is fundamental to the creation of complex modules. As a note,
 * this is related to, but separate from menu.module, which allows menus
 * (which in this context are hierarchical lists of links) to be customized from
 * the Drupal administrative interface.
Dries's avatar
 
Dries committed
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
 *
 * 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
42
 * The found callback function is called with any arguments specified
43
 * in the "page arguments" attribute of its menu item. The
Steven Wittens's avatar
Steven Wittens committed
44 45 46 47
 * 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
48 49 50 51
 *
 * For an illustration of this process, see page_example.module.
 *
 * Access to the callback functions is also protected by the menu system.
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,
54 55 56
 * then access is granted; if FALSE, then access is denied. Default local task
 * menu items (see next paragraph) may omit this attribute to use the value
 * provided by the parent item.
Dries's avatar
 
Dries committed
57 58 59 60 61 62 63 64 65 66 67 68 69
 *
 * 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.
70 71 72
 *
 * Everything described so far is stored in the menu_router table. The
 * menu_links table holds the visible menu links. By default these are
73
 * derived from the same hook_menu definitions, however you are free to
74
 * add more with menu_link_save().
Dries's avatar
 
Dries committed
75 76
 */

Dries's avatar
 
Dries committed
77
/**
78
 * @defgroup menu_flags Menu flags
Dries's avatar
 
Dries committed
79
 * @{
Dries's avatar
 
Dries committed
80 81
 * Flags for use in the "type" attribute of menu items.
 */
Dries's avatar
 
Dries committed
82

83 84 85
/**
 * Internal menu flag -- menu item is the root of the menu tree.
 */
86
const MENU_IS_ROOT = 0x0001;
87 88 89 90

/**
 * Internal menu flag -- menu item is visible in the menu tree.
 */
91
const MENU_VISIBLE_IN_TREE = 0x0002;
92 93 94 95

/**
 * Internal menu flag -- menu item is visible in the breadcrumb.
 */
96
const MENU_VISIBLE_IN_BREADCRUMB = 0x0004;
97 98

/**
99
 * Internal menu flag -- menu item links back to its parent.
100
 */
101
const MENU_LINKS_TO_PARENT = 0x0008;
102 103 104 105

/**
 * Internal menu flag -- menu item can be modified by administrator.
 */
106
const MENU_MODIFIED_BY_ADMIN = 0x0020;
107 108 109 110

/**
 * Internal menu flag -- menu item was created by administrator.
 */
111
const MENU_CREATED_BY_ADMIN = 0x0040;
112 113 114 115

/**
 * Internal menu flag -- menu item is a local task.
 */
116
const MENU_IS_LOCAL_TASK = 0x0080;
Dries's avatar
 
Dries committed
117

118 119 120
/**
 * Internal menu flag -- menu item is a local action.
 */
121
const MENU_IS_LOCAL_ACTION = 0x0100;
122

Dries's avatar
 
Dries committed
123
/**
Dries's avatar
 
Dries committed
124
 * @} End of "Menu flags".
Dries's avatar
 
Dries committed
125 126 127
 */

/**
128
 * @defgroup menu_item_types Menu item types
Dries's avatar
 
Dries committed
129
 * @{
130
 * Definitions for various menu item types.
131
 *
Dries's avatar
 
Dries committed
132
 * Menu item definitions provide one of these constants, which are shortcuts for
133
 * combinations of @link menu_flags Menu flags @endlink.
Dries's avatar
 
Dries committed
134
 */
Dries's avatar
 
Dries committed
135

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

Dries's avatar
 
Dries committed
145
/**
146 147
 * Menu type -- A hidden, internal callback, typically used for API calls.
 *
Dries's avatar
 
Dries committed
148
 * Callbacks simply register a path so that the correct function is fired
149
 * when the URL is accessed. They do not appear in menus or breadcrumbs.
Dries's avatar
 
Dries committed
150
 */
151
const MENU_CALLBACK = 0x0000;
Dries's avatar
 
Dries committed
152

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

/**
164 165 166 167 168
 * 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
169
 */
170
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_VISIBLE_IN_BREADCRUMB);
Dries's avatar
 
Dries committed
171

Dries's avatar
 
Dries committed
172
/**
173 174
 * Menu type -- The "default" local task, which is initially active.
 *
Dries's avatar
 
Dries committed
175 176 177
 * Every set of local tasks should provide one "default" task, that links to the
 * same path as its parent when clicked.
 */
178
define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT | MENU_VISIBLE_IN_BREADCRUMB);
Dries's avatar
 
Dries committed
179

180 181 182 183 184 185
/**
 * 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.
 */
186
define('MENU_LOCAL_ACTION', MENU_IS_LOCAL_TASK | MENU_IS_LOCAL_ACTION | MENU_VISIBLE_IN_BREADCRUMB);
187

Dries's avatar
 
Dries committed
188
/**
Dries's avatar
 
Dries committed
189
 * @} End of "Menu item types".
Dries's avatar
 
Dries committed
190 191
 */

192
/**
193
 * @defgroup menu_context_types Menu context types
194 195 196 197
 * @{
 * Flags for use in the "context" attribute of menu router items.
 */

198 199 200 201 202 203
/**
 * 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.
 */
204
const MENU_CONTEXT_NONE = 0x0000;
205

206 207 208
/**
 * Internal menu flag: Local task should be displayed in page context.
 */
209
const MENU_CONTEXT_PAGE = 0x0001;
210 211 212 213

/**
 * Internal menu flag: Local task should be displayed inline.
 */
214
const MENU_CONTEXT_INLINE = 0x0002;
215 216 217 218 219

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

Dries's avatar
 
Dries committed
220
/**
221
 * @defgroup menu_status_codes Menu status codes
Dries's avatar
 
Dries committed
222
 * @{
Dries's avatar
 
Dries committed
223 224
 * Status codes for menu callbacks.
 */
Dries's avatar
 
Dries committed
225

226 227 228
/**
 * Internal menu status code -- Menu item was not found.
 */
229
const MENU_NOT_FOUND = 404;
230 231 232 233

/**
 * Internal menu status code -- Menu item access is denied.
 */
234
const MENU_ACCESS_DENIED = 403;
235 236 237 238

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

241 242 243
/**
 * Internal menu status code -- Everything is working fine.
 */
244
const MENU_SITE_ONLINE = 5;
245

Dries's avatar
 
Dries committed
246
/**
Dries's avatar
 
Dries committed
247
 * @} End of "Menu status codes".
Dries's avatar
 
Dries committed
248
 */
249

250
/**
251
 * @defgroup menu_tree_parameters Menu tree parameters
252
 * @{
253
 * Parameters for a menu tree.
254 255
 */

256 257
 /**
 * The maximum number of path elements for a menu callback
258
 */
259
const MENU_MAX_PARTS = 9;
260

261 262

/**
263
 * The maximum depth of a menu links tree - matches the number of p columns.
264
 */
265
const MENU_MAX_DEPTH = 9;
266

267 268

/**
269
 * @} End of "Menu tree parameters".
270 271
 */

272 273 274 275 276 277 278 279 280 281 282 283 284 285
/**
 * Reserved key to identify the most specific menu link for a given path.
 *
 * The value of this constant is a hash of the constant name. We use the hash
 * so that the reserved key is over 32 characters in length and will not
 * collide with allowed menu names:
 * @code
 * sha1('MENU_PREFERRED_LINK') = 1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91
 * @endcode
 *
 * @see menu_link_get_preferred()
 */
const MENU_PREFERRED_LINK = '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91';

286
/**
287 288 289
 * Returns the ancestors (and relevant placeholders) for any given path.
 *
 * For example, the ancestors of node/12345/edit are:
290 291 292 293 294 295 296
 * - node/12345/edit
 * - node/12345/%
 * - node/%/edit
 * - node/%/%
 * - node/12345
 * - node/%
 * - node
297 298 299 300 301
 *
 * 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
302
 * any argument matches that part. We limit ourselves to using binary
303
 * numbers that correspond the patterns of wildcards of router items that
304
 * actually exists. This list of 'masks' is built in menu_rebuild().
305 306 307 308
 *
 * @param $parts
 *   An array of path parts, for the above example
 *   array('node', '12345', 'edit').
309
 *
310 311
 * @return
 *   An array which contains the ancestors and placeholders. Placeholders
312
 *   simply contain as many '%s' as the ancestors.
313 314
 */
function menu_get_ancestors($parts) {
315
  $number_parts = count($parts);
316
  $ancestors = array();
317 318
  $length =  $number_parts - 1;
  $end = (1 << $number_parts) - 1;
319 320 321 322 323 324 325 326
  $masks = variable_get('menu_masks');
  // If the optimized menu_masks array is not available use brute force to get
  // the correct $ancestors and $placeholders returned. Do not use this as the
  // default value of the menu_masks variable to avoid building such a big
  // array.
  if (!$masks) {
    $masks = range(511, 1);
  }
327 328 329 330 331 332 333 334 335 336
  // 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;
    }
337 338
    $current = '';
    for ($j = $length; $j >= 0; $j--) {
339
      // Check the bit on the $j offset.
340
      if ($i & (1 << $j)) {
341
        // Bit one means the original value.
342 343 344
        $current .= $parts[$length - $j];
      }
      else {
345
        // Bit zero means means wildcard.
346 347
        $current .= '%';
      }
348
      // Unless we are at offset 0, add a slash.
349 350 351
      if ($j) {
        $current .= '/';
      }
Dries's avatar
 
Dries committed
352
    }
353
    $ancestors[] = $current;
354
  }
355
  return $ancestors;
Dries's avatar
 
Dries committed
356 357 358
}

/**
359
 * Unserializes menu data, using a map to replace path elements.
Dries's avatar
 
Dries committed
360
 *
361 362 363 364 365 366
 * The menu system stores various path-related information (such as the 'page
 * arguments' and 'access arguments' components of a menu item) in the database
 * using serialized arrays, where integer values in the arrays represent
 * arguments to be replaced by values from the path. This function first
 * unserializes such menu information arrays, and then does the path
 * replacement.
367
 *
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
 * The path replacement acts on each integer-valued element of the unserialized
 * menu data array ($data) using a map array ($map, which is typically an array
 * of path arguments) as a list of replacements. For instance, if there is an
 * element of $data whose value is the number 2, then it is replaced in $data
 * with $map[2]; non-integer values in $data are left alone.
 *
 * As an example, an unserialized $data array with elements ('node_load', 1)
 * represents instructions for calling the node_load() function. Specifically,
 * this instruction says to use the path component at index 1 as the input
 * parameter to node_load(). If the path is 'node/123', then $map will be the
 * array ('node', 123), and the returned array from this function will have
 * elements ('node_load', 123), since $map[1] is 123. This return value will
 * indicate specifically that node_load(123) is to be called to load the node
 * whose ID is 123 for this menu item.
 *
 * @param $data
 *   A serialized array of menu data, as read from the database.
 * @param $map
 *   A path argument array, used to replace integer values in $data; an integer
 *   value N in $data will be replaced by value $map[N]. Typically, the $map
 *   array is generated from a call to the arg() function.
389
 *
390
 * @return
391
 *   The unserialized $data array, with path arguments replaced.
392
 */
393 394 395 396 397 398 399 400
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;
401
  }
402 403
  else {
    return array();
404 405 406
  }
}

407 408


409
/**
410
 * Replaces the statically cached item for a given path.
411
 *
412
 * @param $path
413 414 415 416 417 418
 *   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.
419
 */
420 421 422 423 424 425 426 427 428 429 430 431
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.
432
 *
433 434 435 436
 * @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.
437 438 439
 *   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.
440 441
 */
function menu_get_item($path = NULL, $router_item = NULL) {
442
  $router_items = &drupal_static(__FUNCTION__);
443 444 445
  if (!isset($path)) {
    $path = $_GET['q'];
  }
446 447 448
  if (isset($router_item)) {
    $router_items[$path] = $router_item;
  }
449
  if (!isset($router_items[$path])) {
450 451 452 453 454
    // 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();
    }
455
    $original_map = arg(NULL, $path);
456

457 458 459 460
    $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
    $ancestors = menu_get_ancestors($parts);
    $router_item = db_query_range('SELECT * FROM {menu_router} WHERE path IN (:ancestors) ORDER BY fit DESC', 0, 1, array(':ancestors' => $ancestors))->fetchAssoc();

461
    if ($router_item) {
462 463 464 465
      // Allow modules to alter the router item before it is translated and
      // checked for access.
      drupal_alter('menu_get_item', $router_item, $path, $original_map);

466
      $map = _menu_translate($router_item, $original_map);
467
      $router_item['original_map'] = $original_map;
468
      if ($map === FALSE) {
469
        $router_items[$path] = FALSE;
470
        return FALSE;
471
      }
472 473 474
      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']));
475
        $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
476 477
      }
    }
478
    $router_items[$path] = $router_item;
Dries's avatar
 
Dries committed
479
  }
480
  return $router_items[$path];
Dries's avatar
 
Dries committed
481 482 483
}

/**
484
 * Execute the page callback associated with the current path.
485
 *
486 487 488 489 490 491 492 493 494
 * @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) {
495 496 497 498 499 500 501 502 503 504 505
  // Check if site is offline.
  $page_callback_result = _menu_site_is_offline() ? MENU_SITE_OFFLINE : MENU_SITE_ONLINE;

  // Allow other modules to change the site status but not the path because that
  // would not change the global variable. hook_url_inbound_alter() can be used
  // to change the path. Code later will not use the $read_only_path variable.
  $read_only_path = !empty($path) ? $path : $_GET['q'];
  drupal_alter('menu_site_status', $page_callback_result, $read_only_path);

  // Only continue if the site status is not set.
  if ($page_callback_result == MENU_SITE_ONLINE) {
506 507
    if ($router_item = menu_get_item($path)) {
      if ($router_item['access']) {
508 509
        if ($router_item['include_file']) {
          require_once DRUPAL_ROOT . '/' . $router_item['include_file'];
510 511 512 513 514
        }
        $page_callback_result = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
      }
      else {
        $page_callback_result = MENU_ACCESS_DENIED;
515 516 517
      }
    }
    else {
518
      $page_callback_result = MENU_NOT_FOUND;
519
    }
Dries's avatar
 
Dries committed
520
  }
521 522 523 524 525 526 527 528 529 530

  // 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;
  }
531
}
Dries's avatar
 
Dries committed
532

533
/**
534
 * Loads objects into the map as defined in the $item['load_functions'].
535
 *
536
 * @param $item
537
 *   A menu router or menu link item
538 539
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
540
 *
541
 * @return
542 543
 *   Returns TRUE for success, FALSE if an object cannot be loaded.
 *   Names of object loading functions are placed in $item['load_functions'].
544
 *   Loaded objects are placed in $map[]; keys are the same as keys in the
545 546
 *   $item['load_functions'] array.
 *   $item['access'] is set to FALSE if an object cannot be loaded.
547
 */
548 549 550
function _menu_load_objects(&$item, &$map) {
  if ($load_functions = $item['load_functions']) {
    // If someone calls this function twice, then unserialize will fail.
551 552
    if (!is_array($load_functions)) {
      $load_functions = unserialize($load_functions);
553
    }
554 555 556
    $path_map = $map;
    foreach ($load_functions as $index => $function) {
      if ($function) {
557 558 559 560 561 562 563
        $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);
564
          $load_functions[$index] = $function;
565 566 567

          // Some arguments are placeholders for dynamic items to process.
          foreach ($args as $i => $arg) {
568
            if ($arg === '%index') {
569
              // Pass on argument index to the load function, so multiple
570
              // occurrences of the same placeholder can be identified.
571 572
              $args[$i] = $index;
            }
573
            if ($arg === '%map') {
574 575 576 577 578
              // 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;
            }
579 580 581
            if (is_int($arg)) {
              $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : '';
            }
582 583 584 585 586 587 588
          }
          array_unshift($args, $value);
          $return = call_user_func_array($function, $args);
        }
        else {
          $return = $function($value);
        }
589 590
        // If callback returned an error or there is no callback, trigger 404.
        if ($return === FALSE) {
591
          $item['access'] = FALSE;
592
          $map = FALSE;
593
          return FALSE;
594 595 596 597
        }
        $map[$index] = $return;
      }
    }
598
    $item['load_functions'] = $load_functions;
599
  }
600 601 602 603 604 605 606
  return TRUE;
}

/**
 * Check access to a menu item using the access callback
 *
 * @param $item
607
 *   A menu router or menu link item
608 609
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
610
 *
611
 * @return
612
 *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
613 614
 */
function _menu_check_access(&$item, $map) {
615 616
  // Determine access callback, which will decide whether or not the current
  // user has access to this path.
617
  $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']);
618
  // Check for a TRUE or FALSE value.
619
  if (is_numeric($callback)) {
620
    $item['access'] = (bool) $callback;
Dries's avatar
 
Dries committed
621
  }
622
  else {
623
    $arguments = menu_unserialize($item['access_arguments'], $map);
624 625 626
    // 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') {
627
      $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
628
    }
629
    else {
630
      $item['access'] = call_user_func_array($callback, $arguments);
631
    }
Dries's avatar
 
Dries committed
632
  }
633
}
634

635
/**
636
 * Localize the router item title using t() or another callback.
637
 *
638 639 640 641 642 643 644 645 646 647 648 649 650
 * 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.
651
 *
652 653 654
 * @return
 *   No return value.
 *   $item['title'] is localized according to $item['title_callback'].
655
 *   If an item's callback is check_plain(), $item['options']['html'] becomes
656 657
 *   TRUE.
 *   $item['description'] is translated using t().
658
 *   When doing link translation and the $item['options']['attributes']['title']
659
 *   (link title attribute) matches the description, it is translated as well.
660 661
 */
function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
662
  $callback = $item['title_callback'];
663
  $item['localized_options'] = $item['options'];
664 665 666 667 668 669 670 671
  // 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']);
  }
672 673 674 675 676 677 678
  // 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'])) {
679 680 681 682 683 684 685 686 687
    // 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));
      }
688
    }
689
    elseif ($callback) {
690 691 692 693 694 695
      if (empty($item['title_arguments'])) {
        $item['title'] = $callback($item['title']);
      }
      else {
        $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
      }
696 697
      // Avoid calling check_plain again on l() function.
      if ($callback == 'check_plain') {
698
        $item['localized_options']['html'] = TRUE;
699
      }
700 701
    }
  }
702 703
  elseif ($link_translate) {
    $item['title'] = $item['link_title'];
704 705 706
  }

  // Translate description, see the motivation above.
707
  if (!empty($item['description'])) {
708
    $original_description = $item['description'];
709
    $item['description'] = t($item['description']);
710
    if ($link_translate && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) {
711
      $item['localized_options']['attributes']['title'] = $item['description'];
712
    }
713
  }
714 715 716 717 718 719 720 721 722 723 724 725 726 727 728
}

/**
 * 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
729
 * to the language required to generate the current page.
730
 *
731 732
 * @param $router_item
 *   A menu router item
733 734 735
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @param $to_arg
736
 *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
737
 *   path from the menu table, for example tabs.
738
 *
739 740
 * @return
 *   Returns the map with objects loaded as defined in the
741
 *   $item['load_functions']. $item['access'] becomes TRUE if the item is
742
 *   accessible, FALSE otherwise. $item['href'] is set according to the map.
743 744 745
 *   If an error occurs during calling the load_functions (like trying to load
 *   a non existing node) then this function return FALSE.
 */
746
function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
747
  if ($to_arg && !empty($router_item['to_arg_functions'])) {
748 749 750 751 752
    // 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.
753
  $path_map = $map;
754
  if (!empty($router_item['load_functions']) && !_menu_load_objects($router_item, $map)) {
755
    // An error occurred loading an object.
756
    $router_item['access'] = FALSE;
757 758 759 760
    return FALSE;
  }

  // Generate the link path for the page request or local tasks.
761
  $link_map = explode('/', $router_item['path']);
762 763 764 765 766 767
  if (isset($router_item['tab_root'])) {
    $tab_root_map = explode('/', $router_item['tab_root']);
  }
  if (isset($router_item['tab_parent'])) {
    $tab_parent_map = explode('/', $router_item['tab_parent']);
  }
768
  for ($i = 0; $i < $router_item['number_parts']; $i++) {
769 770 771
    if ($link_map[$i] == '%') {
      $link_map[$i] = $path_map[$i];
    }
772 773 774 775 776 777
    if (isset($tab_root_map[$i]) && $tab_root_map[$i] == '%') {
      $tab_root_map[$i] = $path_map[$i];
    }
    if (isset($tab_parent_map[$i]) && $tab_parent_map[$i] == '%') {
      $tab_parent_map[$i] = $path_map[$i];
    }
778
  }
779
  $router_item['href'] = implode('/', $link_map);
780 781
  $router_item['tab_root_href'] = implode('/', $tab_root_map);
  $router_item['tab_parent_href'] = implode('/', $tab_parent_map);
782
  $router_item['options'] = array();
783
  _menu_check_access($router_item, $map);
784

785 786 787 788
  // For performance, don't localize an item the user can't access.
  if ($router_item['access']) {
    _menu_item_localize($router_item, $map);
  }
789 790 791 792 793 794 795 796 797

  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.
 *
798
 * @param $map
799 800
 *   An array of path arguments (ex: array('node', '5'))
 * @param $to_arg_functions
801
 *   An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
802 803
 */
function _menu_link_map_translate(&$map, $to_arg_functions) {
804 805 806 807 808 809 810 811 812
  $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]);
813 814 815 816
    }
  }
}

817 818 819
/**
 * Returns path as one string from the argument we are currently at.
 */
820 821 822 823
function menu_tail_to_arg($arg, $map, $index) {
  return implode('/', array_slice($map, $index));
}

824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840
/**
 * Loads path as one string from the argument we are currently at.
 *
 * To use this load function, you must specify the load arguments
 * in the router item as:
 * @code
 * $item['load arguments'] = array('%map', '%index');
 * @endcode
 *
 * @see search_menu().
 */
function menu_tail_load($arg, &$map, $index) {
  $arg = implode('/', array_slice($map, $index));
  $map = array_slice($map, 0, $index);
  return $arg;
}

841 842 843 844 845
/**
 * This function is similar to _menu_translate() but does link-specific
 * preparation such as always calling to_arg functions
 *
 * @param $item
846 847 848 849 850
 *   A menu link.
 * @param $translate
 *   (optional) Whether to try to translate a link containing dynamic path
 *   argument placeholders (%) based on the menu router item of the current
 *   path. Defaults to FALSE. Internally used for breadcrumbs.
851
 *
852 853
 * @return
 *   Returns the map of path arguments with objects loaded as defined in the
854 855 856 857
 *   $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.
858
 *   $item['options'] is unserialized; it is also changed within the call here
859
 *   to $item['localized_options'] by _menu_item_localize().
860
 */
861 862 863 864
function _menu_link_translate(&$item, $translate = FALSE) {
  if (!is_array($item['options'])) {
    $item['options'] = unserialize($item['options']);
  }
865 866
  if ($item['external']) {
    $item['access'] = 1;
867
    $map = array();
868 869
    $item['href'] = $item['link_path'];
    $item['title'] = $item['link_title'];
870
    $item['localized_options'] = $item['options'];
871 872
  }
  else {
873 874
    // Complete the path of the menu link with elements from the current path,
    // if it contains dynamic placeholders (%).
875
    $map = explode('/', $item['link_path']);
876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902
    if (strpos($item['link_path'], '%') !== FALSE) {
      // Invoke registered to_arg callbacks.
      if (!empty($item['to_arg_functions'])) {
        _menu_link_map_translate($map, $item['to_arg_functions']);
      }
      // Or try to derive the path argument map from the current router item,
      // if this $item's path is within the router item's path. This means
      // that if we are on the current path 'foo/%/bar/%/baz', then
      // menu_get_item() will have translated the menu router item for the
      // current path, and we can take over the argument map for a link like
      // 'foo/%/bar'. This inheritance is only valid for breadcrumb links.
      // @see _menu_tree_check_access()
      // @see menu_get_active_breadcrumb()
      elseif ($translate && ($current_router_item = menu_get_item())) {
        // If $translate is TRUE, then this link is in the active trail.
        // Only translate paths within the current path.
        if (strpos($current_router_item['path'], $item['link_path']) === 0) {
          $count = count($map);
          $map = array_slice($current_router_item['original_map'], 0, $count);
          $item['original_map'] = $map;
          if (isset($current_router_item['map'])) {
            $item['map'] = array_slice($current_router_item['map'], 0, $count);
          }
          // Reset access to check it (for the first time).
          unset($item['access']);
        }
      }
903
    }
904
    $item['href'] = implode('/', $map);
905

906
    // Skip links containing untranslated arguments.
907 908
    if (strpos($item['href'], '%') !== FALSE) {
      $item['access'] = FALSE;
909 910
      return FALSE;
    }
911
    // menu_tree_check_access() may set this ahead of time for links to nodes.
912
    if (!isset($item['access'])) {
913
      if (!empty($item['load_functions']) && !_menu_load_objects($item, $map)) {
914
        // An error occurred loading an object.
915 916 917
        $item['access'] = FALSE;
        return FALSE;
      }
918 919
      _menu_check_access($item, $map);
    }
920 921 922 923
    // For performance, don't localize a link the user can't access.
    if ($item['access']) {
      _menu_item_localize($item, $map, TRUE);
    }
924
  }
925

926 927 928 929 930 931
  // 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);
  }
932

933
  return $map;
Dries's avatar
 
Dries committed
934 935
}

936 937 938
/**
 * Get a loaded object from a router item.
 *
939 940 941 942 943 944 945 946 947 948
 * 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
949 950
 *
 * @param $type
951
 *   Type of the object. These appear in hook_menu definitions as %type. Core
952 953 954 955
 *   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
956 957 958
 *   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.
959
 * @param $path
960
 *   See menu_get_item() for more on this. Defaults to the current path.
961 962 963
 */
function menu_get_object($type = 'node', $position = 1, $path = NULL) {
  $router_item = menu_get_item($path);
964
  if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type . '_load') {
965 966 967 968
    return $router_item['map'][$position];
  }
}

969
/**
970
 * Renders a menu tree based on the current path.
971 972
 *
 * The tree is expanded based on the current path and dynamic paths are also
973
 * changed according to the defined to_arg functions (for example the 'My
974
 * account' link is changed from user/% to a link with the current user's uid).
975 976 977
 *
 * @param $menu_name
 *   The name of the menu.
978
 *
979
 * @return
980 981
 *   A structured array representing the specified menu on the current page, to
 *   be rendered by drupal_render().
982
 */
983
function menu_tree($menu_name) {
984
  $menu_output = &drupal_static(__FUNCTION__, array());
985 986

  if (!isset($menu_output[$menu_name])) {
987
    $tree = menu_tree_page_data($menu_name);
988 989 990 991 992
    $menu_output[$menu_name] = menu_tree_output($tree);
  }
  return $menu_output[$menu_name];
}

Dries's avatar
 
Dries committed
993
/**
994
 * Returns a rendered menu tree.
995
 *
996 997 998 999 1000
 * 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.
 *
1001 1002
 * @param $tree
 *   A data structure representing the tree as returned from menu_tree_data.
1003
 *
1004
 * @return
1005
 *   A structured array to be rendered by drupal_render().
Dries's avatar
 
Dries committed
1006
 */
1007
function menu_tree_output($tree) {
1008
  $build = array();
1009
  $items = array();
1010

1011