menu.inc 63.8 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
define('MENU_LINKS_TO_PARENT', 0x0008);
Dries's avatar
   
Dries committed
84
define('MENU_MODIFIABLE_BY_ADMIN', 0x0010);
85
86
87
define('MENU_MODIFIED_BY_ADMIN', 0x0020);
define('MENU_CREATED_BY_ADMIN', 0x0040);
define('MENU_IS_LOCAL_TASK', 0x0080);
Dries's avatar
   
Dries committed
88

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

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

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

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

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

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

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

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

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

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

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

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

162
163

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

168
169

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

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

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

/**
271
 * Get the menu callback for the a path.
272
 *
273
 * @param $path
274
 *   A path, or NULL for the current path
275
 */
276
function menu_get_item($path = NULL) {
277
  static $router_items;
278
279
280
  if (!isset($path)) {
    $path = $_GET['q'];
  }
281
  if (!isset($router_items[$path])) {
282
    $original_map = arg(NULL, $path);
283
    $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
284
    list($ancestors, $placeholders) = menu_get_ancestors($parts);
285

286
287
    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);
288
      if ($map === FALSE) {
289
        $router_items[$path] = FALSE;
290
        return FALSE;
291
      }
292
293
294
      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
295
296
      }
    }
297
    $router_items[$path] = $router_item;
Dries's avatar
   
Dries committed
298
  }
299
  return $router_items[$path];
Dries's avatar
   
Dries committed
300
301
302
}

/**
303
 * Execute the page callback associated with the current path
Dries's avatar
   
Dries committed
304
 */
305
function menu_execute_active_handler($path = NULL) {
306
307
  if (_menu_site_is_offline()) {
    return MENU_SITE_OFFLINE;
308
309
  }
  if ($router_item = menu_get_item($path)) {
310
311
312
    if ($router_item['access']) {
      if ($router_item['file']) {
        require_once($router_item['file']);
313
      }
314
      return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
315
316
317
318
    }
    else {
      return MENU_ACCESS_DENIED;
    }
Dries's avatar
   
Dries committed
319
  }
320
321
  return MENU_NOT_FOUND;
}
Dries's avatar
   
Dries committed
322

323
/**
324
 * Loads objects into the map as defined in the $item['load_functions'].
325
 *
326
 * @param $item
327
 *   A menu router or menu link item
328
329
330
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
331
332
333
 *   Returns TRUE for success, FALSE if an object cannot be loaded
 */
function _menu_load_objects($item, &$map) {
334
335
  if ($item['load_functions']) {
    $load_functions = unserialize($item['load_functions']);
336
337
338
339
340
    $path_map = $map;
    foreach ($load_functions as $index => $function) {
      if ($function) {

        $return = $function(isset($path_map[$index]) ? $path_map[$index] : '');
341
342
        // If callback returned an error or there is no callback, trigger 404.
        if ($return === FALSE) {
343
          $item['access'] = FALSE;
344
          $map = FALSE;
345
          return FALSE;
346
347
348
349
350
        }
        $map[$index] = $return;
      }
    }
  }
351
352
353
354
355
356
357
  return TRUE;
}

/**
 * Check access to a menu item using the access callback
 *
 * @param $item
358
 *   A menu router or menu link item
359
360
361
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
362
 *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
363
364
 */
function _menu_check_access(&$item, $map) {
365
366
  // Determine access callback, which will decide whether or not the current
  // user has access to this path.
367
  $callback = trim($item['access_callback']);
368
  // Check for a TRUE or FALSE value.
369
  if (is_numeric($callback)) {
370
    $item['access'] = $callback;
Dries's avatar
   
Dries committed
371
  }
372
  else {
373
    $arguments = menu_unserialize($item['access_arguments'], $map);
374
375
376
    // 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') {
377
      $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
378
379
    }
    else {
380
      $item['access'] = call_user_func_array($callback, $arguments);
381
    }
Dries's avatar
   
Dries committed
382
  }
383
}
384

385
386
387
/**
 * Localize the item title using t() or another callback.
 */
388
function _menu_item_localize(&$item, $map) {
389
390
391
  // 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.
392
  $callback = $item['title_callback'];
393
394
395
  // 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') {
396
397
    if (empty($item['title_arguments'])) {
      $item['title'] = t($item['title']);
398
399
    }
    else {
400
      $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
401
402
403
    }
  }
  else {
404
405
    if (empty($item['title_arguments'])) {
      $item['title'] = $callback($item['title']);
406
407
    }
    else {
408
      $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
409
410
411
412
    }
  }

  // Translate description, see the motivation above.
413
414
  if (!empty($item['description'])) {
    $item['description'] = t($item['description']);
415
  }
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
}

/**
 * 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
 *
433
434
 * @param $router_item
 *   A menu router item
435
436
437
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @param $to_arg
438
 *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
439
440
441
 *   path from the menu table, for example tabs.
 * @return
 *   Returns the map with objects loaded as defined in the
442
443
 *   $item['load_functions. $item['access'] becomes TRUE if the item is
 *   accessible, FALSE otherwise. $item['href'] is set according to the map.
444
445
446
 *   If an error occurs during calling the load_functions (like trying to load
 *   a non existing node) then this function return FALSE.
 */
447
function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
448
  $path_map = $map;
449
  if (!_menu_load_objects($router_item, $map)) {
450
    // An error occurred loading an object.
451
    $router_item['access'] = FALSE;
452
453
454
    return FALSE;
  }
  if ($to_arg) {
455
    _menu_link_map_translate($path_map, $router_item['to_arg_functions']);
456
457
458
  }

  // Generate the link path for the page request or local tasks.
459
460
  $link_map = explode('/', $router_item['path']);
  for ($i = 0; $i < $router_item['number_parts']; $i++) {
461
462
463
464
    if ($link_map[$i] == '%') {
      $link_map[$i] = $path_map[$i];
    }
  }
465
466
  $router_item['href'] = implode('/', $link_map);
  _menu_check_access($router_item, $map);
467

468
  _menu_item_localize($router_item, $map);
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503

  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
 *   An array of helper function (ex: array(1 => 'node_load'))
 */
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.
      $arg = $function(!empty($map[$index]) ? $map[$index] : '');
      if (!empty($map[$index]) || isset($arg)) {
        $map[$index] = $arg;
      }
      else {
        unset($map[$index]);
      }
    }
  }
}

/**
 * This function is similar to _menu_translate() but does link-specific
 * preparation such as always calling to_arg functions
 *
 * @param $item
504
 *   A menu link
505
506
 * @return
 *   Returns the map of path arguments with objects loaded as defined in the
507
508
509
510
 *   $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.
511
512
 */
function _menu_link_translate(&$item) {
513
514
  if ($item['external']) {
    $item['access'] = 1;
515
    $map = array();
516
517
    $item['href'] = $item['link_path'];
    $item['title'] = $item['link_title'];
518
519
  }
  else {
520
521
522
    $map = explode('/', $item['link_path']);
    _menu_link_map_translate($map, $item['to_arg_functions']);
    $item['href'] = implode('/', $map);
523

524
    // Note - skip callbacks without real values for their arguments.
525
526
    if (strpos($item['href'], '%') !== FALSE) {
      $item['access'] = FALSE;
527
528
      return FALSE;
    }
529
    // menu_tree_check_access() may set this ahead of time for links to nodes.
530
531
    if (!isset($item['access'])) {
      if (!_menu_load_objects($item, $map)) {
532
        // An error occured loading an object.
533
534
535
        $item['access'] = FALSE;
        return FALSE;
      }
536
537
538
      _menu_check_access($item, $map);
    }
    // If the link title matches that of a router item, localize it.
539
540
    if (!empty($item['title']) && (($item['title'] == $item['link_title']) || ($item['title_callback'] != 't'))) {
      _menu_item_localize($item, $map);
541
542
543
    }
    else {
      $item['title'] = $item['link_title'];
544
545
    }
  }
546
  $item['options'] = unserialize($item['options']);
547

548
  return $map;
Dries's avatar
   
Dries committed
549
550
}

551
/**
552
553
554
555
556
 * 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).
557
558
559
560
561
562
563
564
565
566
 *
 * @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])) {
567
    $tree = menu_tree_page_data($menu_name);
568
569
570
571
572
    $menu_output[$menu_name] = menu_tree_output($tree);
  }
  return $menu_output[$menu_name];
}

Dries's avatar
   
Dries committed
573
/**
574
 * Returns a rendered menu tree.
575
576
577
578
579
 *
 * @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
580
 */
581
582
583
584
function menu_tree_output($tree) {
  $output = '';

  foreach ($tree as $data) {
585
    if (!$data['link']['hidden']) {
586
587
      $link = theme('menu_item_link', $data['link']);
      if ($data['below']) {
588
        $output .= theme('menu_item', $link, $data['link']['has_children'], menu_tree_output($data['below']), $data['link']['in_active_trail']);
589
590
      }
      else {
591
        $output .= theme('menu_item', $link, $data['link']['has_children'], '', $data['link']['in_active_trail']);
592
      }
593
    }
594
595
596
597
  }
  return $output ? theme('menu_tree', $output) : '';
}

598
/**
599
600
601
602
 * 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.
603
604
605
606
607
608
609
610
611
612
 *
 * @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.
 */
613
function menu_tree_all_data($menu_name = 'navigation', $item = NULL) {
614
615
  static $tree = array();

616
  // Use $mlid as a flag for whether the data being loaded is for the whole tree.
617
  $mlid = isset($item['mlid']) ? $item['mlid'] : 0;
618
  // Generate the cache ID.
619
  $cid = 'links:'. $menu_name .':all:'. $mlid;
620
621

  if (!isset($tree[$cid])) {
622
    // If the static variable doesn't have the data, check {cache_menu}.
623
624
    $cache = cache_get($cid, 'cache_menu');
    if ($cache && isset($cache->data)) {
625
      $data = $cache->data;
626
627
    }
    else {
628
      // Build and run the query, and build the tree.
629
      if ($mlid) {
630
631
        // 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.
632
633
634
635
        $args = array(0);
        for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
          $args[] = $item["p$i"];
        }
636
637
638
639
640
641
642
        $args = array_unique($args);
        $placeholders = implode(', ', array_fill(0, count($args), '%d'));
        $where = ' AND ml.plid IN ('. $placeholders .')';
        $parents = $args;
        $parents[] = $item['mlid'];
      }
      else {
643
        // Get all links in this menu.
644
645
646
647
648
        $where = '';
        $args = array();
        $parents = array();
      }
      array_unshift($args, $menu_name);
649
      // Select the links from the table, and recursively build the tree.  We
650
      // LEFT JOIN since there is no match in {menu_router} for an external
651
      // link.
652
653
      $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.*
654
        FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
655
656
        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);
657
658
      $data['node_links'] = array();
      menu_tree_collect_node_links($data['tree'], $data['node_links']);
659
      // Cache the data.
660
      cache_set($cid, $data, 'cache_menu');
661
    }
662
    // Check access for the current user to each item in the tree.
663
    menu_tree_check_access($data['tree'], $data['node_links']);
664
    $tree[$cid] = $data['tree'];
665
666
667
668
669
  }

  return $tree[$cid];
}

670
/**
671
672
673
 * 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
674
675
676
677
678
679
680
681
 * 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
682
683
 *   submenu below the link if there is one, and it is a subtree that has the
 *   same structure described for the top-level array.
684
 */
685
function menu_tree_page_data($menu_name = 'navigation') {
686
687
  static $tree = array();

688
  // Load the menu item corresponding to the current page.
689
  if ($item = menu_get_item()) {
690
    // Generate the cache ID.
691
    $cid = 'links:'. $menu_name .':page:'. $item['href'] .':'. (int)$item['access'];
692

693
    if (!isset($tree[$cid])) {
694
      // If the static variable doesn't have the data, check {cache_menu}.
695
696
      $cache = cache_get($cid, 'cache_menu');
      if ($cache && isset($cache->data)) {
697
        $data = $cache->data;
698
699
      }
      else {
700
        // Build and run the query, and build the tree.
701
        if ($item['access']) {
702
          // Check whether a menu link exists that corresponds to the current path.
703
          $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']));
704

705
          if (empty($parents)) {
706
            // If no link exists, we may be on a local task that's not in the links.
707
            // TODO: Handle the case like a local task on a specific node in the menu.
708
            $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']));
709
          }
710
          // We always want all the top-level links with plid == 0.
711
712
          $parents[] = '0';

713
714
          // Use array_values() so that the indices are numeric for array_merge().
          $args = $parents = array_unique(array_values($parents));
715
716
          $placeholders = implode(', ', array_fill(0, count($args), '%d'));
          $expanded = variable_get('menu_expanded', array());
717
          // Check whether the current menu has any links set to be expanded.
718
          if (in_array($menu_name, $expanded)) {
719
720
            // Collect all the links set to be expanded, and then add all of
            // their children to the list as well.
721
            do {
722
              $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));
723
              $num_rows = FALSE;
724
725
              while ($item = db_fetch_array($result)) {
                $args[] = $item['mlid'];
726
                $num_rows = TRUE;
727
728
              }
              $placeholders = implode(', ', array_fill(0, count($args), '%d'));
729
            } while ($num_rows);
730
731
732
733
          }
          array_unshift($args, $menu_name);
        }
        else {
734
735
          // Show only the top-level menu items when access is denied.
          $args = array($menu_name, '0');
736
737
738
          $placeholders = '%d';
          $parents = array();
        }
739
740
        // 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
741
        // link.
742
743
        $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.*
744
          FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
745
746
          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);
747
748
        $data['node_links'] = array();
        menu_tree_collect_node_links($data['tree'], $data['node_links']);
749
        // Cache the data.
750
        cache_set($cid, $data, 'cache_menu');
751
      }
752
      // Check access for the current user to each item in the tree.
753
754
      menu_tree_check_access($data['tree'], $data['node_links']);
      $tree[$cid] = $data['tree'];
755
    }
756
    return $tree[$cid];
757
  }
758
759

  return array();
Dries's avatar
   
Dries committed
760
761
}

762
763
764
765
766
767
768
769
770
771
772
773
774
775
/**
 * 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)) {
        $node_links[$nid] = &$tree[$key]['link'];
        $tree[$key]['link']['access'] = FALSE;
      }
    }
    if ($tree[$key]['below']) {
776
      menu_tree_collect_node_links($tree[$key]['below'], $node_links);
777
778
779
780
    }
  }
}

781
782
783
/**
 * Check access and perform other dynamic operations for each link in the tree.
 */
784
function menu_tree_check_access(&$tree, $node_links = array()) {
785
786
787
788

  if ($node_links) {
    // Use db_rewrite_sql to evaluate view access without loading each full node.
    $nids = array_keys($node_links);
789
    $placeholders = '%d'. str_repeat(', %d', count($nids) - 1);
790
791
792
793
794
    $result = db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.nid IN (". $placeholders .")"), $nids);
    while ($node = db_fetch_array($result)) {
      $node_links[$node['nid']]['access'] = TRUE;
    }
  }
795
  _menu_tree_check_access($tree);
796
  return;
797
798
799
800
801
}

/**
 * Recursive helper function for menu_tree_check_access()
 */
802
function _menu_tree_check_access(&$tree) {
803
  $new_tree = array();
804
805
  foreach ($tree as $key => $v) {
    $item = &$tree[$key]['link'];
806
    _menu_link_translate($item);
807
808
    if ($item['access']) {
      if ($tree[$key]['below']) {
809
        _menu_tree_check_access($tree[$key]['below']);
810
811
812
813
814
      }
      // 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];
815
816
    }
  }
817
818
819
  // Sort siblings in the tree based on the weights and localized titles.
  ksort($new_tree);
  $tree = $new_tree;
820
}
821

822
/**
823
 * Build the data representing a menu tree.
824
825
826
 *
 * @param $result
 *   The database result.
827
828
829
 * @param $parents
 *   An array of the plid values that represent the path from the current page
 *   to the root of the menu tree.
830
831
832
 * @param $depth
 *   The depth of the current menu tree.
 * @return
833
834
835
836
837
838
839
840
841
842
843
844
845
846
 *   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.
847
 */
848
function _menu_tree_data($result, $parents, $depth, $previous_element = '') {
849
  $remnant = NULL;
850
  $tree = array();
851
  while ($item = db_fetch_array($result)) {
852
853
    // We need to determine if we're on the path to root so we can later build
    // the correct active trail and breadcrumb.
854
    $item['in_active_trail'] = in_array($item['mlid'], $parents);
855
856

    $index = $previous_element ? ($previous_element['mlid']) : '';
857
    // The current item is the first in a new submenu.
858
    if ($item['depth'] > $depth) {
859
      // _menu_tree returns an item and the menu tree structure.
860
      list($item, $below) = _menu_tree_data($result, $parents, $item['depth'], $item);
861
862
863
864
865
      $tree[$index] = array(
        'link' => $previous_element,
        'below' => $below,
      );
      // We need to fall back one level.
866
      if (!isset($item) || $item['depth'] < $depth) {
867
868
        return array($item, $tree);
      }
869
      // This will be the link to be output in the next iteration.
870
      $previous_element = $item;
Dries's avatar
   
Dries committed
871
    }
872
    // We are at the same depth, so we use the previous element.
873
    elseif ($item['depth'] == $depth) {
874
875
      if ($previous_element) {
        // Only the first time.
876
877
        $tree[$index] = array(
          'link' => $previous_element,
878
          'below' => FALSE,
879
880
        );
      }
881
      // This will be the link to be output in the next iteration.
882
      $previous_element = $item;
Dries's avatar
   
Dries committed
883
    }
884
    // The submenu ended with the previous item, so pass back the current item.
885
    else {
886
      $remnant = $item;
887
      break;
Dries's avatar
   
Dries committed
888
    }
Dries's avatar
   
Dries committed
889
  }
890
  if ($previous_element) {
891
    // We have one more link dangling.
892
    $tree[$previous_element['mlid']] = array(
893
      'link' => $previous_element,
894
      'below' => FALSE,
895
    );
896
897
  }
  return array($remnant, $tree);
Dries's avatar
   
Dries committed
898
899
}

Dries's avatar
   
Dries committed
900
/**
901
 * Generate the HTML output for a single menu link.
Dries's avatar
   
Dries committed
902
 */
903
function theme_menu_item_link($link) {
904
  return l($link['title'], $link['href'], $link['options']);
Dries's avatar
   
Dries committed
905
906
}

Dries's avatar
   
Dries committed
907
/**
908
 * Generate the HTML output for a menu tree
Dries's avatar
   
Dries committed
909
 */
910
911
912
913
914
915
916
function theme_menu_tree($tree) {
  return '<ul class="menu">'. $tree .'</ul>';
}

/**
 * Generate the HTML output for a menu item and submenu.
 */
917
918
919
920
921
922
function theme_menu_item($link, $has_children, $menu = '', $in_active_trail = FALSE) {
  $class = ($menu ? 'expanded' : ($has_children ? 'collapsed' : 'leaf'));
  if ($in_active_trail) {
    $class .= ' active-trail';
  }
  return '<li class="'. $class .'">'. $link . $menu .'</li>'."\n";
923
924
}

925
926
927
/**
 * Generate the HTML output for a single local task link.
 */
928
function theme_menu_local_task($link, $active = FALSE) {
929
  return '<li '. ($active ? 'class="active" ' : '') .'>'. $link .'</li>';
930
}
Dries's avatar
   
Dries committed
931

932
933
934
935
936
937
938
939
/**
 * 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
940
/**
Dries's avatar
   
Dries committed
941
942
 * Returns the help associated with the active menu item.
 */
943
function menu_get_active_help() {
Dries's avatar
   
Dries committed
944
  $output = '';
945
  $router_path = menu_tab_root_path();
Dries's avatar
   
Dries committed
946

947
948
  $arg = drupal_help_arg(arg(NULL));
  $empty_arg = drupal_help_arg();
Dries's avatar
   
Dries committed
949

Dries's avatar
   
Dries committed
950
951
  foreach (module_list() as $name) {
    if (module_hook($name, 'help')) {
952
953
954
      // Lookup help for this path.
      if ($help = module_invoke($name, 'help', $router_path, $arg)) {
        $output .= $help ."\n";
Dries's avatar
   
Dries committed
955
      }
956
957
      // Add "more help" link on admin pages if the module provides a
      // standalone help page.
958
959
      if (module_hook('help', 'page') && $arg[0] == "admin" && module_invoke($name, 'help', 'admin/help#'. $arg[2], $empty_arg) && $help) {
        $output .= theme("more_help_link", url('admin/help/'. $arg[2]));
Dries's avatar
   
Dries committed
960
      }
Dries's avatar
   
Dries committed
961
    }
Dries's avatar
   
Dries committed
962
  }
Dries's avatar
   
Dries committed
963
  return $output;
964
965
}

966
/**
Dries's avatar