menu.inc 77.3 KB
Newer Older
Dries's avatar
   
Dries committed
1
<?php
2
// $Id$
Kjartan's avatar
Kjartan committed
3

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

Dries's avatar
   
Dries committed
9
10
11
/**
 * @defgroup menu Menu system
 * @{
Dries's avatar
   
Dries committed
12
 * Define the navigation menus, and route page requests to code based on URLs.
Dries's avatar
   
Dries committed
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 *
 * The Drupal menu system drives both the navigation system from a user
 * perspective and the callback system that Drupal uses to respond to URLs
 * passed from the browser. For this reason, a good understanding of the
 * menu system is fundamental to the creation of complex modules.
 *
 * Drupal's menu system follows a simple hierarchy defined by paths.
 * Implementations of hook_menu() define menu items and assign them to
 * paths (which should be unique). The menu system aggregates these items
 * and determines the menu hierarchy from the paths. For example, if the
 * paths defined were a, a/b, e, a/b/c/d, f/g, and a/b/h, the menu system
 * would form the structure:
 * - a
 *   - a/b
 *     - a/b/c/d
 *     - a/b/h
 * - e
 * - f/g
 * Note that the number of elements in the path does not necessarily
 * determine the depth of the menu item in the tree.
 *
 * When responding to a page request, the menu system looks to see if the
 * path requested by the browser is registered as a menu item with a
 * callback. If not, the system searches up the menu tree for the most
 * complete match with a callback it can find. If the path a/b/i is
 * requested in the tree above, the callback for a/b would be used.
 *
Steven Wittens's avatar
Steven Wittens committed
40
 * The found callback function is called with any arguments specified
41
 * in the "page arguments" attribute of its menu item. The
Steven Wittens's avatar
Steven Wittens committed
42
43
44
45
 * attribute must be an array. After these arguments, any remaining
 * components of the path are appended as further arguments. In this
 * way, the callback for a/b above could respond to a request for
 * a/b/i differently than a request for a/b/j.
Dries's avatar
   
Dries committed
46
47
48
49
 *
 * For an illustration of this process, see page_example.module.
 *
 * Access to the callback functions is also protected by the menu system.
50
51
52
53
 * The "access callback" with an optional "access arguments" of each menu
 * item is called before the page callback proceeds. If this returns TRUE,
 * then access is granted; if FALSE, then access is denied. Menu items may
 * omit this attribute to use the value provided by an ancestor item.
Dries's avatar
   
Dries committed
54
55
56
57
58
59
60
61
62
63
64
65
66
 *
 * In the default Drupal interface, you will notice many links rendered as
 * tabs. These are known in the menu system as "local tasks", and they are
 * rendered as tabs by default, though other presentations are possible.
 * Local tasks function just as other menu items in most respects. It is
 * convention that the names of these tasks should be short verbs if
 * possible. In addition, a "default" local task should be provided for
 * each set. When visiting a local task's parent menu item, the default
 * local task will be rendered as if it is selected; this provides for a
 * normal tab user experience. This default task is special in that it
 * links not to its provided path, but to its parent item's path instead.
 * The default task's path is only used to place it appropriately in the
 * menu hierarchy.
67
68
69
 *
 * Everything described so far is stored in the menu_router table. The
 * menu_links table holds the visible menu links. By default these are
70
 * derived from the same hook_menu definitions, however you are free to
71
 * add more with menu_link_save().
Dries's avatar
   
Dries committed
72
73
 */

Dries's avatar
   
Dries committed
74
/**
Dries's avatar
   
Dries committed
75
 * @name Menu flags
Dries's avatar
   
Dries committed
76
 * @{
Dries's avatar
   
Dries committed
77
78
 * Flags for use in the "type" attribute of menu items.
 */
Dries's avatar
   
Dries committed
79

Dries's avatar
   
Dries committed
80
81
82
define('MENU_IS_ROOT', 0x0001);
define('MENU_VISIBLE_IN_TREE', 0x0002);
define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004);
83
84
85
86
define('MENU_LINKS_TO_PARENT', 0x0008);
define('MENU_MODIFIED_BY_ADMIN', 0x0020);
define('MENU_CREATED_BY_ADMIN', 0x0040);
define('MENU_IS_LOCAL_TASK', 0x0080);
Dries's avatar
   
Dries committed
87

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

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

Dries's avatar
   
Dries committed
99
100
/**
 * Normal menu items show up in the menu tree and can be moved/hidden by
Dries's avatar
   
Dries committed
101
102
 * the administrator. Use this for most menu items. It is the default value if
 * no menu item type is specified.
Dries's avatar
   
Dries committed
103
 */
104
define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);
105

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

Dries's avatar
   
Dries committed
112
/**
Dries's avatar
   
Dries committed
113
114
 * Modules may "suggest" menu items that the administrator may enable. They act
 * just as callbacks do until enabled, at which time they act like normal items.
115
116
 * Note for the value: 0x0010 was a flag which is no longer used, but this way
 * the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate.
Dries's avatar
   
Dries committed
117
 */
118
define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010);
Dries's avatar
   
Dries committed
119
120

/**
Dries's avatar
   
Dries committed
121
122
123
 * Local tasks are rendered as tabs by default. Use this for menu items that
 * describe actions to be performed on their parent item. An example is the path
 * "node/52/edit", which performs the "edit" task on "node/52".
Dries's avatar
   
Dries committed
124
125
126
 */
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK);

Dries's avatar
   
Dries committed
127
128
129
130
131
132
/**
 * Every set of local tasks should provide one "default" task, that links to the
 * same path as its parent when clicked.
 */
define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT);

Dries's avatar
   
Dries committed
133
/**
Dries's avatar
   
Dries committed
134
 * @} End of "Menu item types".
Dries's avatar
   
Dries committed
135
136
137
138
139
 */

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

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

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

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

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

163
164

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

169
170

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

174
/**
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
 * Returns the ancestors (and relevant placeholders) for any given path.
 *
 * For example, the ancestors of node/12345/edit are:
 *
 * node/12345/edit
 * node/12345/%
 * node/%/edit
 * node/%/%
 * node/12345
 * node/%
 * node
 *
 * To generate these, we will use binary numbers. Each bit represents a
 * part of the path. If the bit is 1, then it represents the original
 * value while 0 means wildcard. If the path is node/12/edit/foo
 * then the 1011 bitstring represents node/%/edit/foo where % means that
191
192
193
 * any argument matches that part.  We limit ourselves to using binary
 * numbers that correspond the patterns of wildcards of router items that
 * actually exists.  This list of 'masks' is built in menu_rebuild().
194
195
196
197
198
199
 *
 * @param $parts
 *   An array of path parts, for the above example
 *   array('node', '12345', 'edit').
 * @return
 *   An array which contains the ancestors and placeholders. Placeholders
200
 *   simply contain as many '%s' as the ancestors.
201
202
 */
function menu_get_ancestors($parts) {
203
  $number_parts = count($parts);
204
205
  $placeholders = array();
  $ancestors = array();
206
207
208
209
210
211
212
213
214
215
216
217
218
  $length =  $number_parts - 1;
  $end = (1 << $number_parts) - 1;
  $masks = variable_get('menu_masks', array());
  // Only examine patterns that actually exist as router items (the masks).
  foreach ($masks as $i) {
    if ($i > $end) {
      // Only look at masks that are not longer than the path of interest.
      continue;
    }
    elseif ($i < (1 << $length)) {
      // We have exhausted the masks of a given length, so decrease the length.
      --$length;
    }
219
220
221
222
223
224
225
226
227
228
229
    $current = '';
    for ($j = $length; $j >= 0; $j--) {
      if ($i & (1 << $j)) {
        $current .= $parts[$length - $j];
      }
      else {
        $current .= '%';
      }
      if ($j) {
        $current .= '/';
      }
Dries's avatar
   
Dries committed
230
    }
231
232
    $placeholders[] = "'%s'";
    $ancestors[] = $current;
233
  }
234
  return array($ancestors, $placeholders);
Dries's avatar
   
Dries committed
235
236
237
}

/**
238
239
240
241
 * The menu system uses serialized arrays stored in the database for
 * arguments. However, often these need to change according to the
 * current path. This function unserializes such an array and does the
 * necessary change.
Dries's avatar
   
Dries committed
242
 *
243
 * Integer values are mapped according to the $map parameter. For
244
245
246
247
248
 * example, if unserialize($data) is array('view', 1) and $map is
 * array('node', '12345') then 'view' will not be changed because
 * it is not an integer, but 1 will as it is an integer. As $map[1]
 * is '12345', 1 will be replaced with '12345'. So the result will
 * be array('node_load', '12345').
249
 *
250
251
252
253
 * @param @data
 *   A serialized array.
 * @param @map
 *   An array of potential replacements.
254
 * @return
255
 *   The $data array unserialized and mapped.
256
 */
257
258
259
260
261
262
263
264
function menu_unserialize($data, $map) {
  if ($data = unserialize($data)) {
    foreach ($data as $k => $v) {
      if (is_int($v)) {
        $data[$k] = isset($map[$v]) ? $map[$v] : '';
      }
    }
    return $data;
265
  }
266
267
  else {
    return array();
268
269
270
  }
}

271
272


273
/**
274
 * Replaces the statically cached item for a given path.
275
 *
276
 * @param $path
277
278
279
280
281
282
 *   The path.
 * @param $router_item
 *   The router item. Usually you take a router entry from menu_get_item and
 *   set it back either modified or to a different path. This lets you modify the
 *   navigation block, the page title, the breadcrumb and the page help in one
 *   call.
283
 */
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
function menu_set_item($path, $router_item) {
  menu_get_item($path, $router_item);
}

/**
 * Get a router item.
 *
 * @param $path
 *   The path, for example node/5. The function will find the corresponding
 *   node/% item and return that.
 * @param $router_item
 *   Internal use only.
 * @return
 *   The router item, an associate array corresponding to one row in the
 *   menu_router table. The value of key map holds the loaded objects. The
 *   value of key access is TRUE if the current user can access this page.
 *   The values for key title, page_arguments, access_arguments will be
 *   filled in based on the database values and the objects loaded.
 */
function menu_get_item($path = NULL, $router_item = NULL) {
304
  static $router_items;
305
306
307
  if (!isset($path)) {
    $path = $_GET['q'];
  }
308
309
310
  if (isset($router_item)) {
    $router_items[$path] = $router_item;
  }
311
  if (!isset($router_items[$path])) {
312
    $original_map = arg(NULL, $path);
313
    $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
314
    list($ancestors, $placeholders) = menu_get_ancestors($parts);
315

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

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

353
/**
354
 * Loads objects into the map as defined in the $item['load_functions'].
355
 *
356
 * @param $item
357
 *   A menu router or menu link item
358
359
360
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @return
361
362
 *   Returns TRUE for success, FALSE if an object cannot be loaded
 */
363
364
365
366
367
368
function _menu_load_objects(&$item, &$map) {
  if ($load_functions = $item['load_functions']) {
    // If someone calls this function twice, then unserialize will fail.
    if ($load_functions_unserialized = unserialize($load_functions)) {
      $load_functions = $load_functions_unserialized;
    }
369
370
371
    $path_map = $map;
    foreach ($load_functions as $index => $function) {
      if ($function) {
372
373
374
375
376
377
378
        $value = isset($path_map[$index]) ? $path_map[$index] : '';
        if (is_array($function)) {
          // Set up arguments for the load function. These were pulled from
          // 'load arguments' in the hook_menu() entry, but they need
          // some processing. In this case the $function is the key to the
          // load_function array, and the value is the list of arguments.
          list($function, $args) = each($function);
379
          $load_functions[$index] = $function;
380
381
382

          // Some arguments are placeholders for dynamic items to process.
          foreach ($args as $i => $arg) {
383
            if ($arg === '%index') {
384
385
386
387
              // Pass on argument index to the load function, so multiple
              // occurances of the same placeholder can be identified.
              $args[$i] = $index;
            }
388
            if ($arg === '%map') {
389
390
391
392
393
              // Pass on menu map by reference. The accepting function must
              // also declare this as a reference if it wants to modify
              // the map.
              $args[$i] = &$map;
            }
394
395
396
            if (is_int($arg)) {
              $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : '';
            }
397
398
399
400
401
402
403
          }
          array_unshift($args, $value);
          $return = call_user_func_array($function, $args);
        }
        else {
          $return = $function($value);
        }
404
405
        // If callback returned an error or there is no callback, trigger 404.
        if ($return === FALSE) {
406
          $item['access'] = FALSE;
407
          $map = FALSE;
408
          return FALSE;
409
410
411
412
        }
        $map[$index] = $return;
      }
    }
413
    $item['load_functions'] = $load_functions;
414
  }
415
416
417
418
419
420
421
  return TRUE;
}

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

449
450
451
/**
 * Localize the item title using t() or another callback.
 */
452
function _menu_item_localize(&$item, $map) {
453
454
455
  // Translate the title to allow storage of English title strings in the
  // database, yet display of them in the language required by the current
  // user.
456
  $callback = $item['title_callback'];
457
458
459
  // t() is a special case. Since it is used very close to all the time,
  // we handle it directly instead of using indirect, slower methods.
  if ($callback == 't') {
460
461
    if (empty($item['title_arguments'])) {
      $item['title'] = t($item['title']);
462
463
    }
    else {
464
      $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
465
466
    }
  }
467
  elseif ($callback) {
468
469
    if (empty($item['title_arguments'])) {
      $item['title'] = $callback($item['title']);
470
471
    }
    else {
472
      $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
473
474
475
476
    }
  }

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

/**
 * Handles dynamic path translation and menu access control.
 *
 * When a user arrives on a page such as node/5, this function determines
 * what "5" corresponds to, by inspecting the page's menu path definition,
 * node/%node. This will call node_load(5) to load the corresponding node
 * object.
 *
 * It also works in reverse, to allow the display of tabs and menu items which
 * contain these dynamic arguments, translating node/%node to node/5.
 *
 * Translation of menu item titles and descriptions are done here to
 * allow for storage of English strings in the database, and translation
 * to the language required to generate the current page
 *
497
498
 * @param $router_item
 *   A menu router item
499
500
501
 * @param $map
 *   An array of path arguments (ex: array('node', '5'))
 * @param $to_arg
502
 *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
503
504
505
 *   path from the menu table, for example tabs.
 * @return
 *   Returns the map with objects loaded as defined in the
506
507
 *   $item['load_functions. $item['access'] becomes TRUE if the item is
 *   accessible, FALSE otherwise. $item['href'] is set according to the map.
508
509
510
 *   If an error occurs during calling the load_functions (like trying to load
 *   a non existing node) then this function return FALSE.
 */
511
function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
512
  $path_map = $map;
513
  if (!_menu_load_objects($router_item, $map)) {
514
    // An error occurred loading an object.
515
    $router_item['access'] = FALSE;
516
517
518
    return FALSE;
  }
  if ($to_arg) {
519
    _menu_link_map_translate($path_map, $router_item['to_arg_functions']);
520
521
522
  }

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

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

  return $map;
}

/**
 * This function translates the path elements in the map using any to_arg
 * helper function. These functions take an argument and return an object.
 * See http://drupal.org/node/109153 for more information.
 *
 * @param map
 *   An array of path arguments (ex: array('node', '5'))
 * @param $to_arg_functions
546
 *   An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
547
548
549
550
551
552
 */
function _menu_link_map_translate(&$map, $to_arg_functions) {
  if ($to_arg_functions) {
    $to_arg_functions = unserialize($to_arg_functions);
    foreach ($to_arg_functions as $index => $function) {
      // Translate place-holders into real values.
553
      $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
554
555
556
557
558
559
560
561
562
563
      if (!empty($map[$index]) || isset($arg)) {
        $map[$index] = $arg;
      }
      else {
        unset($map[$index]);
      }
    }
  }
}

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

568
569
570
571
572
/**
 * This function is similar to _menu_translate() but does link-specific
 * preparation such as always calling to_arg functions
 *
 * @param $item
573
 *   A menu link
574
575
 * @return
 *   Returns the map of path arguments with objects loaded as defined in the
576
577
578
579
 *   $item['load_functions'].
 *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
 *   $item['href'] is generated from link_path, possibly by to_arg functions.
 *   $item['title'] is generated from link_title, and may be localized.
580
581
 */
function _menu_link_translate(&$item) {
582
  $item['options'] = unserialize($item['options']);
583
584
  if ($item['external']) {
    $item['access'] = 1;
585
    $map = array();
586
587
    $item['href'] = $item['link_path'];
    $item['title'] = $item['link_title'];
588
589
  }
  else {
590
591
592
    $map = explode('/', $item['link_path']);
    _menu_link_map_translate($map, $item['to_arg_functions']);
    $item['href'] = implode('/', $map);
593

594
    // Note - skip callbacks without real values for their arguments.
595
596
    if (strpos($item['href'], '%') !== FALSE) {
      $item['access'] = FALSE;
597
598
      return FALSE;
    }
599
    // menu_tree_check_access() may set this ahead of time for links to nodes.
600
601
    if (!isset($item['access'])) {
      if (!_menu_load_objects($item, $map)) {
602
        // An error occurred loading an object.
603
604
605
        $item['access'] = FALSE;
        return FALSE;
      }
606
607
      _menu_check_access($item, $map);
    }
608

609
    // If the link title matches that of its router item, localize it.
610
    if (!empty($item['title']) && ($item['title'] == $item['link_title'])) {
611
612
613
614
615
616
      if (!empty($item['title_arguments']) && $item['title_callback'] == 't') {
        $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
      }
      else {
        $item['title'] = t($item['title']);
      }
617
618
619
    }
    else {
      $item['title'] = $item['link_title'];
620
    }
621
622
623
624
625
626
627
628
629
630
631
632
633
    // Localize the description and title attribute.
    if (!empty($item['description'])) {
      $original_description = $item['description'];
      $item['description'] = t($item['description']);
      // Localize the title attribute only if it matches the description.
      if ($item['options']['attributes']['title'] == $original_description) {
        $item['options']['attributes']['title'] = $item['description'];
      }
    }
    // Store the map if it may be needed for use later by a title callback.
    if (!empty($item['title_callback']) && ($item['title_callback'] != 't')) {
      $item['map'] = $map;
    }
634
  }
635
  return $map;
Dries's avatar
   
Dries committed
636
637
}

638
639
640
641
642
/**
 * 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
643
644
645
 * 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.
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
 *
 * @param $type
 *   Type of the object. These appear in hook_menu definitons as %type. Core
 *   provides aggregator_feed, aggregator_category, contact, filter_format,
 *   forum_term, menu, menu_link, node, taxonomy_vocabulary, user. See the
 *   relevant {$type}_load function for more on each. Defaults to node.
 * @param $position
 *   The expected position for $type object. For node/%node this is 1, for
 *   comment/reply/%node this is 2. Defaults to 1.
 * @param $path
 *   See @menu_get_item for more on this. Defaults to the current path.
 */
function menu_get_object($type = 'node', $position = 1, $path = NULL) {
  $router_item = menu_get_item($path);
  if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type .'_load') {
    return $router_item['map'][$position];
  }
}

665
/**
666
667
668
669
670
 * 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).
671
672
673
674
675
676
677
678
679
680
 *
 * @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])) {
681
    $tree = menu_tree_page_data($menu_name);
682
683
684
685
686
    $menu_output[$menu_name] = menu_tree_output($tree);
  }
  return $menu_output[$menu_name];
}

Dries's avatar
   
Dries committed
687
/**
688
 * Returns a rendered menu tree.
689
690
691
692
693
 *
 * @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
694
 */
695
696
function menu_tree_output($tree) {
  $output = '';
697
  $items = array();
698

699
  // Pull out just the menu items we are going to render so that we
700
  // get an accurate count for the first/last classes.
701
  foreach ($tree as $data) {
702
    if (!$data['link']['hidden']) {
703
704
705
      $items[] = $data;
    }
  }
706

707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
  $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);
722
    }
723
724
725
726
  }
  return $output ? theme('menu_tree', $output) : '';
}

727
/**
728
729
730
731
 * 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.
732
733
734
735
736
737
738
739
740
741
 *
 * @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.
 */
742
function menu_tree_all_data($menu_name = 'navigation', $item = NULL) {
743
744
  static $tree = array();

745
  // Use $mlid as a flag for whether the data being loaded is for the whole tree.
746
  $mlid = isset($item['mlid']) ? $item['mlid'] : 0;
747
  // Generate the cache ID.
748
  $cid = 'links:'. $menu_name .':all:'. $mlid;
749
750

  if (!isset($tree[$cid])) {
751
    // If the static variable doesn't have the data, check {cache_menu}.
752
753
    $cache = cache_get($cid, 'cache_menu');
    if ($cache && isset($cache->data)) {
754
      $data = $cache->data;
755
756
    }
    else {
757
      // Build and run the query, and build the tree.
758
      if ($mlid) {
759
760
        // 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.
761
762
763
764
        $args = array(0);
        for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
          $args[] = $item["p$i"];
        }
765
766
767
768
769
770
771
        $args = array_unique($args);
        $placeholders = implode(', ', array_fill(0, count($args), '%d'));
        $where = ' AND ml.plid IN ('. $placeholders .')';
        $parents = $args;
        $parents[] = $item['mlid'];
      }
      else {
772
        // Get all links in this menu.
773
774
775
776
777
        $where = '';
        $args = array();
        $parents = array();
      }
      array_unshift($args, $menu_name);
778
      // Select the links from the table, and recursively build the tree.  We
779
      // LEFT JOIN since there is no match in {menu_router} for an external
780
      // link.
781
      $data['tree'] = menu_tree_data(db_query("
782
        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.*
783
        FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
784
785
        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);
786
787
      $data['node_links'] = array();
      menu_tree_collect_node_links($data['tree'], $data['node_links']);
788
      // Cache the data.
789
      cache_set($cid, $data, 'cache_menu');
790
    }
791
    // Check access for the current user to each item in the tree.
792
    menu_tree_check_access($data['tree'], $data['node_links']);
793
    $tree[$cid] = $data['tree'];
794
795
796
797
798
  }

  return $tree[$cid];
}

799
/**
800
801
802
 * 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
803
804
805
806
807
808
809
810
 * 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
811
812
 *   submenu below the link if there is one, and it is a subtree that has the
 *   same structure described for the top-level array.
813
 */
814
function menu_tree_page_data($menu_name = 'navigation') {
815
816
  static $tree = array();

817
  // Load the menu item corresponding to the current page.
818
  if ($item = menu_get_item()) {
819
    // Generate the cache ID.
820
    $cid = 'links:'. $menu_name .':page:'. $item['href'] .':'. (int)$item['access'];
821

822
    if (!isset($tree[$cid])) {
823
      // If the static variable doesn't have the data, check {cache_menu}.
824
825
      $cache = cache_get($cid, 'cache_menu');
      if ($cache && isset($cache->data)) {
826
        $data = $cache->data;
827
828
      }
      else {
829
        // Build and run the query, and build the tree.
830
        if ($item['access']) {
831
          // Check whether a menu link exists that corresponds to the current path.
832
          $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']));
833

834
          if (empty($parents)) {
835
            // If no link exists, we may be on a local task that's not in the links.
836
            // TODO: Handle the case like a local task on a specific node in the menu.
837
            $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']));
838
          }
839
          // We always want all the top-level links with plid == 0.
840
841
          $parents[] = '0';

842
843
          // Use array_values() so that the indices are numeric for array_merge().
          $args = $parents = array_unique(array_values($parents));
844
845
          $placeholders = implode(', ', array_fill(0, count($args), '%d'));
          $expanded = variable_get('menu_expanded', array());
846
          // Check whether the current menu has any links set to be expanded.
847
          if (in_array($menu_name, $expanded)) {
848
849
            // Collect all the links set to be expanded, and then add all of
            // their children to the list as well.
850
            do {
851
              $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));
852
              $num_rows = FALSE;
853
854
              while ($item = db_fetch_array($result)) {
                $args[] = $item['mlid'];
855
                $num_rows = TRUE;
856
857
              }
              $placeholders = implode(', ', array_fill(0, count($args), '%d'));
858
            } while ($num_rows);
859
860
861
862
          }
          array_unshift($args, $menu_name);
        }
        else {
863
864
          // Show only the top-level menu items when access is denied.
          $args = array($menu_name, '0');
865
866
867
          $placeholders = '%d';
          $parents = array();
        }
868
869
        // 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
870
        // link.
871
        $data['tree'] = menu_tree_data(db_query("
872
          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.*
873
          FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
874
875
          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);
876
877
        $data['node_links'] = array();
        menu_tree_collect_node_links($data['tree'], $data['node_links']);
878
        // Cache the data.
879
        cache_set($cid, $data, 'cache_menu');
880
      }
881
      // Check access for the current user to each item in the tree.
882
883
      menu_tree_check_access($data['tree'], $data['node_links']);
      $tree[$cid] = $data['tree'];
884
    }
885
    return $tree[$cid];
886
  }
887
888

  return array();
Dries's avatar
   
Dries committed
889
890
}

891
892
893
894
895
896
897
898
/**
 * 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)) {
899
        $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link'];
900
901
902
903
        $tree[$key]['link']['access'] = FALSE;
      }
    }
    if ($tree[$key]['below']) {
904
      menu_tree_collect_node_links($tree[$key]['below'], $node_links);
905
906
907
908
    }
  }
}

909
910
911
/**
 * Check access and perform other dynamic operations for each link in the tree.
 */
912
function menu_tree_check_access(&$tree, $node_links = array()) {
913
914
915
916

  if ($node_links) {
    // Use db_rewrite_sql to evaluate view access without loading each full node.
    $nids = array_keys($node_links);
917
    $placeholders = '%d'. str_repeat(', %d', count($nids) - 1);
918
919
    $result = db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.nid IN (". $placeholders .")"), $nids);
    while ($node = db_fetch_array($result)) {
920
921
922
923
      $nid = $node['nid'];
      foreach ($node_links[$nid] as $mlid => $link) {
        $node_links[$nid][$mlid]['access'] = TRUE;
      }
924
925
    }
  }
926
  _menu_tree_check_access($tree);
927
  return;
928
929
930
931
932
}

/**
 * Recursive helper function for menu_tree_check_access()
 */
933
function _menu_tree_check_access(&$tree) {
934
  $new_tree = array();
935
936
  foreach ($tree as $key => $v) {
    $item = &$tree[$key]['link'];
937
    _menu_link_translate($item);
938
939
    if ($item['access']) {
      if ($tree[$key]['below']) {
940
        _menu_tree_check_access($tree[$key]['below']);
941
942
943
944
945
      }
      // 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];
946
947
    }
  }
948
949
950
  // Sort siblings in the tree based on the weights and localized titles.
  ksort($new_tree);
  $tree = $new_tree;
951
}
952

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