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

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

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

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

80
81
82
/**
 * Internal menu flag -- menu item is the root of the menu tree.
 */
Dries's avatar
   
Dries committed
83
define('MENU_IS_ROOT', 0x0001);
84
85
86
87

/**
 * Internal menu flag -- menu item is visible in the menu tree.
 */
Dries's avatar
   
Dries committed
88
define('MENU_VISIBLE_IN_TREE', 0x0002);
89
90
91
92

/**
 * Internal menu flag -- menu item is visible in the breadcrumb.
 */
Dries's avatar
   
Dries committed
93
define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004);
94
95
96
97

/**
 * Internal menu flag -- menu item links back to its parnet.
 */
98
define('MENU_LINKS_TO_PARENT', 0x0008);
99
100
101
102

/**
 * Internal menu flag -- menu item can be modified by administrator.
 */
103
define('MENU_MODIFIED_BY_ADMIN', 0x0020);
104
105
106
107

/**
 * Internal menu flag -- menu item was created by administrator.
 */
108
define('MENU_CREATED_BY_ADMIN', 0x0040);
109
110
111
112

/**
 * Internal menu flag -- menu item is a local task.
 */
113
define('MENU_IS_LOCAL_TASK', 0x0080);
Dries's avatar
   
Dries committed
114

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

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

Dries's avatar
   
Dries committed
126
/**
127
128
 * Menu type -- A "normal" menu item that's shown in menu and breadcrumbs.
 *
Dries's avatar
   
Dries committed
129
 * Normal menu items show up in the menu tree and can be moved/hidden by
Dries's avatar
   
Dries committed
130
131
 * the administrator. Use this for most menu items. It is the default value if
 * no menu item type is specified.
Dries's avatar
   
Dries committed
132
 */
133
define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);
134

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

Dries's avatar
   
Dries committed
143
/**
144
145
 * Menu type -- A normal menu item, hidden until enabled by an administrator.
 *
Dries's avatar
   
Dries committed
146
147
 * Modules may "suggest" menu items that the administrator may enable. They act
 * just as callbacks do until enabled, at which time they act like normal items.
148
149
 * Note for the value: 0x0010 was a flag which is no longer used, but this way
 * the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate.
Dries's avatar
   
Dries committed
150
 */
151
define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010);
Dries's avatar
   
Dries committed
152
153

/**
154
155
156
157
158
 * Menu type -- A task specific to the parent item, usually rendered as a tab.
 *
 * Local tasks are menu items that describe actions to be performed on their
 * parent item. An example is the path "node/52/edit", which performs the
 * "edit" task on "node/52".
Dries's avatar
   
Dries committed
159
160
161
 */
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK);

Dries's avatar
   
Dries committed
162
/**
163
164
 * Menu type -- The "default" local task, which is initially active.
 *
Dries's avatar
   
Dries committed
165
166
167
168
169
 * Every set of local tasks should provide one "default" task, that links to the
 * same path as its parent when clicked.
 */
define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT);

Dries's avatar
   
Dries committed
170
/**
Dries's avatar
   
Dries committed
171
 * @} End of "Menu item types".
Dries's avatar
   
Dries committed
172
173
174
175
176
 */

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

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

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

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

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

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

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

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

215
216

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

221
222

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

226
/**
227
228
229
 * Returns the ancestors (and relevant placeholders) for any given path.
 *
 * For example, the ancestors of node/12345/edit are:
230
231
232
233
234
235
236
 * - node/12345/edit
 * - node/12345/%
 * - node/%/edit
 * - node/%/%
 * - node/12345
 * - node/%
 * - node
237
238
239
240
241
 *
 * To generate these, we will use binary numbers. Each bit represents a
 * part of the path. If the bit is 1, then it represents the original
 * value while 0 means wildcard. If the path is node/12/edit/foo
 * then the 1011 bitstring represents node/%/edit/foo where % means that
242
 * any argument matches that part. We limit ourselves to using binary
243
 * numbers that correspond the patterns of wildcards of router items that
244
 * actually exists. This list of 'masks' is built in menu_rebuild().
245
246
247
248
249
250
 *
 * @param $parts
 *   An array of path parts, for the above example
 *   array('node', '12345', 'edit').
 * @return
 *   An array which contains the ancestors and placeholders. Placeholders
251
 *   simply contain as many '%s' as the ancestors.
252
253
 */
function menu_get_ancestors($parts) {
254
  $number_parts = count($parts);
255
  $ancestors = array();
256
257
258
259
260
261
262
263
264
265
266
267
268
  $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;
    }
269
270
271
272
273
274
275
276
277
278
279
    $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
280
    }
281
    $ancestors[] = $current;
282
  }
283
  return $ancestors;
Dries's avatar
   
Dries committed
284
285
286
}

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

320
321


322
/**
323
 * Replaces the statically cached item for a given path.
324
 *
325
 * @param $path
326
327
328
329
330
331
 *   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.
332
 */
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
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) {
353
  static $router_items;
354
355
356
  if (!isset($path)) {
    $path = $_GET['q'];
  }
357
358
359
  if (isset($router_item)) {
    $router_items[$path] = $router_item;
  }
360
  if (!isset($router_items[$path])) {
361
    $original_map = arg(NULL, $path);
362
    $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
363
364
365
366
367
368
369
370
    $ancestors = menu_get_ancestors($parts);
    $router_item = db_select('menu_router')
      ->fields('menu_router')
      ->condition('path', $ancestors, 'IN')
      ->orderBy('fit', 'DESC')
      ->range(0, 1)
      ->execute()->fetchAssoc();
    if ($router_item) {
371
      $map = _menu_translate($router_item, $original_map);
372
      if ($map === FALSE) {
373
        $router_items[$path] = FALSE;
374
        return FALSE;
375
      }
376
377
378
      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
379
380
      }
    }
381
    $router_items[$path] = $router_item;
Dries's avatar
   
Dries committed
382
  }
383
  return $router_items[$path];
Dries's avatar
   
Dries committed
384
385
386
}

/**
387
 * Execute the page callback associated with the current path
Dries's avatar
   
Dries committed
388
 */
389
function menu_execute_active_handler($path = NULL) {
390
391
  if (_menu_site_is_offline()) {
    return MENU_SITE_OFFLINE;
392
  }
393
394
395
  // 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())) {
396
397
    menu_rebuild();
  }
398
  if ($router_item = menu_get_item($path)) {
399
    if ($router_item['access']) {
400
401
      if (drupal_function_exists($router_item['page_callback'])) {
        return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
402
403
404
405
406
      }
    }
    else {
      return MENU_ACCESS_DENIED;
    }
Dries's avatar
   
Dries committed
407
  }
408
409
  return MENU_NOT_FOUND;
}
Dries's avatar
   
Dries committed
410

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

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

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

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

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

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

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

630
631
632
633
  // For performance, don't localize an item the user can't access.
  if ($router_item['access']) {
    _menu_item_localize($router_item, $map);
  }
634
635
636
637
638
639
640
641
642
643
644
645

  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
646
 *   An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
647
648
649
650
651
652
 */
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.
653
      $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
654
655
656
657
658
659
660
661
662
663
      if (!empty($map[$index]) || isset($arg)) {
        $map[$index] = $arg;
      }
      else {
        unset($map[$index]);
      }
    }
  }
}

664
665
666
667
function menu_tail_to_arg($arg, $map, $index) {
  return implode('/', array_slice($map, $index));
}

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

697
    // Note - skip callbacks without real values for their arguments.
698
699
    if (strpos($item['href'], '%') !== FALSE) {
      $item['access'] = FALSE;
700
701
      return FALSE;
    }
702
    // menu_tree_check_access() may set this ahead of time for links to nodes.
703
704
    if (!isset($item['access'])) {
      if (!_menu_load_objects($item, $map)) {
705
        // An error occurred loading an object.
706
707
708
        $item['access'] = FALSE;
        return FALSE;
      }
709
710
      _menu_check_access($item, $map);
    }
711
712
713
714
    // For performance, don't localize a link the user can't access.
    if ($item['access']) {
      _menu_item_localize($item, $map, TRUE);
    }
715
  }
716

717
718
719
720
721
722
  // 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);
  }
723

724
  return $map;
Dries's avatar
   
Dries committed
725
726
}

727
728
729
/**
 * Get a loaded object from a router item.
 *
730
731
732
733
734
735
736
737
738
739
 * 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
740
741
 *
 * @param $type
742
 *   Type of the object. These appear in hook_menu definitions as %type. Core
743
744
745
746
 *   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
747
748
749
 *   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.
750
 * @param $path
751
 *   See menu_get_item() for more on this. Defaults to the current path.
752
753
754
 */
function menu_get_object($type = 'node', $position = 1, $path = NULL) {
  $router_item = menu_get_item($path);
755
  if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type . '_load') {
756
757
758
759
    return $router_item['map'][$position];
  }
}

760
/**
761
762
763
764
765
 * 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).
766
767
768
769
770
771
772
773
774
775
 *
 * @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])) {
776
    $tree = menu_tree_page_data($menu_name);
777
778
779
780
781
    $menu_output[$menu_name] = menu_tree_output($tree);
  }
  return $menu_output[$menu_name];
}

Dries's avatar
   
Dries committed
782
/**
783
 * Returns a rendered menu tree.
784
785
786
787
788
 *
 * @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
789
 */
790
791
function menu_tree_output($tree) {
  $output = '';
792
  $items = array();
793

794
  // Pull out just the menu items we are going to render so that we
795
  // get an accurate count for the first/last classes.
796
  foreach ($tree as $data) {
797
    if (!$data['link']['hidden']) {
798
799
800
      $items[] = $data;
    }
  }
801

802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
  $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);
817
    }
818
819
820
821
  }
  return $output ? theme('menu_tree', $output) : '';
}

822
/**
823
824
825
826
 * 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.
827
828
829
830
 *
 * @param $menu_name
 *   The named menu links to return
 * @param $item
831
 *   A fully loaded menu link, or NULL. If a link is supplied, only the
832
833
834
835
836
 *   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.
 */
837
function menu_tree_all_data($menu_name = 'navigation', $item = NULL) {
838
839
  static $tree = array();

840
  // Use $mlid as a flag for whether the data being loaded is for the whole tree.
841
  $mlid = isset($item['mlid']) ? $item['mlid'] : 0;
842
  // Generate a cache ID (cid) specific for this $menu_name and $item.
843
  $cid = 'links:' . $menu_name . ':all-cid:' . $mlid;
844
845

  if (!isset($tree[$cid])) {
846
    // If the static variable doesn't have the data, check {cache_menu}.
847
848
    $cache = cache_get($cid, 'cache_menu');
    if ($cache && isset($cache->data)) {
849
850
851
852
853
854
      // If the cache entry exists, it will just be the cid for the actual data.
      // This avoids duplication of large amounts of data.
      $cache = cache_get($cache->data, 'cache_menu');
      if ($cache && isset($cache->data)) {
        $data = $cache->data;
      }
855
    }
856
857
    // If the tree data was not in the cache, $data will be NULL.
    if (!isset($data)) {
858
      // Build and run the query, and build the tree.
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
      $query = db_select('menu_links', 'ml');
      $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
      $query->fields('ml');
      $query->fields('m', array(
        'load_functions',
        'to_arg_functions',
        'access_callback',
        'access_arguments',
        'page_callback',
        'page_arguments',
        'title',
        'title_callback',
        'title_arguments',
        'type',
        'description',
      ));
      for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
        $query->orderBy('p' . $i, 'ASC');
      }
      $query->condition('ml.menu_name', $menu_name);

880
      if ($mlid) {
881
882
        // 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.
883
884
885
886
        $args = array(0);
        for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
          $args[] = $item["p$i"];
        }
887
        $args = array_unique($args);
888
        $query->condition('ml.plid', $args, 'IN');
889
890
891
892
        $parents = $args;
        $parents[] = $item['mlid'];
      }
      else {
893
        // Get all links in this menu.
894
895
        $parents = array();
      }
896
      // Select the links from the table, and recursively build the tree. We
897
      // LEFT JOIN since there is no match in {menu_router} for an external
898
      // link.
899
      $data['tree'] = menu_tree_data($query->execute(), $parents);
900
901
      $data['node_links'] = array();
      menu_tree_collect_node_links($data['tree'], $data['node_links']);
902
903
904
905
906
907
908
      // Cache the data, if it is not already in the cache.
      $tree_cid = _menu_tree_cid($menu_name, $data);
      if (!cache_get($tree_cid, 'cache_menu')) {
        cache_set($tree_cid, $data, 'cache_menu');
      }
      // Cache the cid of the (shared) data using the menu and item-specific cid.
      cache_set($cid, $tree_cid, 'cache_menu');
909
    }
910
    // Check access for the current user to each item in the tree.
911
    menu_tree_check_access($data['tree'], $data['node_links']);
912
    $tree[$cid] = $data['tree'];
913
914
915
916
917
  }

  return $tree[$cid];
}

918
/**
919
920
921
 * 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
922
923
924
925
926
927
928
929
 * 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
930
931
 *   submenu below the link if there is one, and it is a subtree that has the
 *   same structure described for the top-level array.
932
 */
933
function menu_tree_page_data($menu_name = 'navigation') {
934
935
  static $tree = array();

936
  // Load the menu item corresponding to the current page.
937
  if ($item = menu_get_item()) {
938
    // Generate a cache ID (cid) specific for this page.
939
    $cid = 'links:' . $menu_name . ':page-cid:' . $item['href'] . ':' . (int)$item['access'];
940

941
    if (!isset($tree[$cid])) {
942
      // If the static variable doesn't have the data, check {cache_menu}.
943
944
      $cache = cache_get($cid, 'cache_menu');
      if ($cache && isset($cache->data)) {
945
946
947
948
949
950
        // If the cache entry exists, it will just be the cid for the actual data.
        // This avoids duplication of large amounts of data.
        $cache = cache_get($cache->data, 'cache_menu');
        if ($cache && isset($cache->data)) {
          $data = $cache->data;
        }
951
      }
952
953
      // If the tree data was not in the cache, $data will be NULL.
      if (!isset($data)) {
954
        // Build and run the query, and build the tree.
955
        if ($item['access']) {
956
          // Check whether a menu link exists that corresponds to the current path.
957
          $args[] = $item['href'];
958
959
960
          if (drupal_is_front_page()) {
            $args[] = '<front>';
          }
961
962
963
964
965
966
967
968
969
970
971
972
973
974
          $parents = db_select('menu_links')
            ->fields('menu_links', array(
              'p1',
              'p2',
              'p3',
              'p4',
              'p5',
              'p6',
              'p7',
              'p8',
            ))
            ->condition('menu_name', $menu_name)
            ->condition('link_path', $args, 'IN')
            ->execute()->fetchAssoc();
975

976
          if (empty($parents)) {
977
            // If no link exists, we may be on a local task that's not in the links.
978
            // TODO: Handle the case like a local task on a specific node in the menu.
979
980
981
982
983
984
985
986
987
988
989
990
991
992
            $parents = db_select('menu_links')
              ->fields('menu_links', array(
                'p1',
                'p2',
                'p3',
                'p4',
                'p5',
                'p6',
                'p7',
                'p8',
              ))
              ->condition('menu_name', $menu_name)
              ->condition('link_path', $item['tab_root'])
              ->execute()->fetchAssoc();
993
          }
994
          // We always want all the top-level links with plid == 0.
995
996
          $parents[] = '0';

997
998
          // Use array_values() so that the indices are numeric for array_merge().
          $args = $parents = array_unique(array_values($parents));
999
          $expanded = variable_get('menu_expanded', array());
1000
          // Check whether the current menu has any links set to be expanded.
1001
          if (in_array($menu_name, $expanded)) {
1002
1003
            // Collect all the links set to be expanded, and then add all of
            // their children to the list as well.
1004
            do {
1005
1006
1007
1008
1009
1010
1011
1012
              $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC))
                ->fields('menu_links', array('mlid'))
                ->condition('menu_name', $menu_name)
                ->condition('expanded', 1)
                ->condition('has_children', 1)
                ->condition('plid', $args, 'IN')
                ->condition('mlid', $args, 'NOT IN')
                ->execute();
1013
              $num_rows = FALSE;
1014
              foreach ($result as $item) {
1015
                $args[] = $item['mlid'];
1016
                $num_rows = TRUE;
1017
              }
1018
            } while ($num_rows);
1019
1020
1021
          }
        }
        else {
1022
          // Show only the top-level menu items when access is denied.
1023
          $args = array(0);
1024
1025
          $parents = array();
        }
1026
1027
        // 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
1028
        // link.
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
        $query = db_select('menu_links', 'ml');
        $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
        $query->fields('ml');
        $query->fields('m', array(
          'load_functions',
          'to_arg_functions',
          'access_callback',
          'access_arguments',
          'page_callback',
          'page_arguments',
          'title',
          'title_callback',
          'title_arguments',
          'type',
          'description',
        ));
        for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
          $query->orderBy('p' . $i, 'ASC');
        }
        $query->condition('ml.menu_name', $menu_name);
        $query->condition('ml.plid', $args, 'IN');
        $data['tree'] = menu_tree_data($query->execute(), $parents);
1051
1052
        $data['node_links'] = array();
        menu_tree_collect_node_links($data['tree'], $data['node_links']);
1053
1054
1055
1056
1057
1058
1059
        // Cache the data, if it is not already in the cache.
        $tree_cid = _menu_tree_cid($menu_name, $data);
        if (!cache_get($tree_cid, 'cache_menu')) {
          cache_set($tree_cid, $data, 'cache_menu');
        }
        // Cache the cid of the (shared) data using the page-specific cid.
        cache_set($cid, $tree_cid, 'cache_menu');
1060
      }
1061
      // Check access for the current user to each item in the tree.
1062
1063
      menu_tree_check_access($data['tree'], $data['node_links']);
      $tree[$cid] = $data['tree'];
1064
    }
1065
    return $tree[$cid];
1066
  }
1067
1068

  return array();
Dries's avatar
   
Dries committed
1069
1070
}

1071
1072
1073
1074
/**
 * Helper function - compute the real cache ID for menu tree data.
 */
function _menu_tree_cid($menu_name, $data) {
1075
  return 'links:' . $menu_name . ':tree-data:' . md5(serialize($data));
1076
1077
}

1078
1079
1080
1081
1082
1083
1084
1085
/**
 * 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)) {
1086
        $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link'];
1087
1088
1089
1090
        $tree[$key]['link']['access'] = FALSE;
      }