Projet

Général

Profil

Paste
Télécharger (136 ko) Statistiques
| Branche: | Révision:

root / htmltest / includes / menu.inc @ 85ad3d82

1
<?php
2

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

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

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

    
83
/**
84
 * Internal menu flag -- menu item is the root of the menu tree.
85
 */
86
define('MENU_IS_ROOT', 0x0001);
87

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

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

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

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

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

    
113
/**
114
 * Internal menu flag -- menu item is a local task.
115
 */
116
define('MENU_IS_LOCAL_TASK', 0x0080);
117

    
118
/**
119
 * Internal menu flag -- menu item is a local action.
120
 */
121
define('MENU_IS_LOCAL_ACTION', 0x0100);
122

    
123
/**
124
 * @} End of "Menu flags".
125
 */
126

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

    
136
/**
137
 * Menu type -- A "normal" menu item that's shown in menu and breadcrumbs.
138
 *
139
 * Normal menu items show up in the menu tree and can be moved/hidden by
140
 * the administrator. Use this for most menu items. It is the default value if
141
 * no menu item type is specified.
142
 */
143
define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);
144

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

    
153
/**
154
 * Menu type -- A normal menu item, hidden until enabled by an administrator.
155
 *
156
 * Modules may "suggest" menu items that the administrator may enable. They act
157
 * just as callbacks do until enabled, at which time they act like normal items.
158
 * Note for the value: 0x0010 was a flag which is no longer used, but this way
159
 * the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate.
160
 */
161
define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010);
162

    
163
/**
164
 * Menu type -- A task specific to the parent item, usually rendered as a tab.
165
 *
166
 * Local tasks are menu items that describe actions to be performed on their
167
 * parent item. An example is the path "node/52/edit", which performs the
168
 * "edit" task on "node/52".
169
 */
170
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_VISIBLE_IN_BREADCRUMB);
171

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

    
180
/**
181
 * Menu type -- An action specific to the parent, usually rendered as a link.
182
 *
183
 * Local actions are menu items that describe actions on the parent item such
184
 * as adding a new user, taxonomy term, etc.
185
 */
186
define('MENU_LOCAL_ACTION', MENU_IS_LOCAL_TASK | MENU_IS_LOCAL_ACTION | MENU_VISIBLE_IN_BREADCRUMB);
187

    
188
/**
189
 * @} End of "Menu item types".
190
 */
191

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

    
198
/**
199
 * Internal menu flag: Invisible local task.
200
 *
201
 * This flag may be used for local tasks like "Delete", so custom modules and
202
 * themes can alter the default context and expose the task by altering menu.
203
 */
204
define('MENU_CONTEXT_NONE', 0x0000);
205

    
206
/**
207
 * Internal menu flag: Local task should be displayed in page context.
208
 */
209
define('MENU_CONTEXT_PAGE', 0x0001);
210

    
211
/**
212
 * Internal menu flag: Local task should be displayed inline.
213
 */
214
define('MENU_CONTEXT_INLINE', 0x0002);
215

    
216
/**
217
 * @} End of "Menu context types".
218
 */
219

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

    
226
/**
227
 * Internal menu status code -- Menu item was found.
228
 */
229
define('MENU_FOUND', 1);
230

    
231
/**
232
 * Internal menu status code -- Menu item was not found.
233
 */
234
define('MENU_NOT_FOUND', 2);
235

    
236
/**
237
 * Internal menu status code -- Menu item access is denied.
238
 */
239
define('MENU_ACCESS_DENIED', 3);
240

    
241
/**
242
 * Internal menu status code -- Menu item inaccessible because site is offline.
243
 */
244
define('MENU_SITE_OFFLINE', 4);
245

    
246
/**
247
 * Internal menu status code -- Everything is working fine.
248
 */
249
define('MENU_SITE_ONLINE', 5);
250

    
251
/**
252
 * @} End of "Menu status codes".
253
 */
254

    
255
/**
256
 * @defgroup menu_tree_parameters Menu tree parameters
257
 * @{
258
 * Parameters for a menu tree.
259
 */
260

    
261
 /**
262
 * The maximum number of path elements for a menu callback
263
 */
264
define('MENU_MAX_PARTS', 9);
265

    
266

    
267
/**
268
 * The maximum depth of a menu links tree - matches the number of p columns.
269
 */
270
define('MENU_MAX_DEPTH', 9);
271

    
272

    
273
/**
274
 * @} End of "Menu tree parameters".
275
 */
276

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

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

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

    
412

    
413

    
414
/**
415
 * Replaces the statically cached item for a given path.
416
 *
417
 * @param $path
418
 *   The path.
419
 * @param $router_item
420
 *   The router item. Usually a router entry from menu_get_item() is either
421
 *   modified or set to a different path. This allows the navigation block,
422
 *   the page title, the breadcrumb, and the page help to be modified in one
423
 *   call.
424
 */
425
function menu_set_item($path, $router_item) {
426
  menu_get_item($path, $router_item);
427
}
428

    
429
/**
430
 * Gets a router item.
431
 *
432
 * @param $path
433
 *   The path; for example, 'node/5'. The function will find the corresponding
434
 *   node/% item and return that.
435
 * @param $router_item
436
 *   Internal use only.
437
 *
438
 * @return
439
 *   The router item or, if an error occurs in _menu_translate(), FALSE. A
440
 *   router item is an associative array corresponding to one row in the
441
 *   menu_router table. The value corresponding to the key 'map' holds the
442
 *   loaded objects. The value corresponding to the key 'access' is TRUE if the
443
 *   current user can access this page. The values corresponding to the keys
444
 *   'title', 'page_arguments', 'access_arguments', and 'theme_arguments' will
445
 *   be filled in based on the database values and the objects loaded.
446
 */
447
function menu_get_item($path = NULL, $router_item = NULL) {
448
  $router_items = &drupal_static(__FUNCTION__);
449
  if (!isset($path)) {
450
    $path = $_GET['q'];
451
  }
452
  if (isset($router_item)) {
453
    $router_items[$path] = $router_item;
454
  }
455
  if (!isset($router_items[$path])) {
456
    // Rebuild if we know it's needed, or if the menu masks are missing which
457
    // occurs rarely, likely due to a race condition of multiple rebuilds.
458
    if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) {
459
      menu_rebuild();
460
    }
461
    $original_map = arg(NULL, $path);
462

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

    
467
    if ($router_item) {
468
      // Allow modules to alter the router item before it is translated and
469
      // checked for access.
470
      drupal_alter('menu_get_item', $router_item, $path, $original_map);
471

    
472
      $map = _menu_translate($router_item, $original_map);
473
      $router_item['original_map'] = $original_map;
474
      if ($map === FALSE) {
475
        $router_items[$path] = FALSE;
476
        return FALSE;
477
      }
478
      if ($router_item['access']) {
479
        $router_item['map'] = $map;
480
        $router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $map), array_slice($map, $router_item['number_parts']));
481
        $router_item['theme_arguments'] = array_merge(menu_unserialize($router_item['theme_arguments'], $map), array_slice($map, $router_item['number_parts']));
482
      }
483
    }
484
    $router_items[$path] = $router_item;
485
  }
486
  return $router_items[$path];
487
}
488

    
489
/**
490
 * Execute the page callback associated with the current path.
491
 *
492
 * @param $path
493
 *   The drupal path whose handler is to be be executed. If set to NULL, then
494
 *   the current path is used.
495
 * @param $deliver
496
 *   (optional) A boolean to indicate whether the content should be sent to the
497
 *   browser using the appropriate delivery callback (TRUE) or whether to return
498
 *   the result to the caller (FALSE).
499
 */
500
function menu_execute_active_handler($path = NULL, $deliver = TRUE) {
501
  // Check if site is offline.
502
  $page_callback_result = _menu_site_is_offline() ? MENU_SITE_OFFLINE : MENU_SITE_ONLINE;
503

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

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

    
528
  // Deliver the result of the page callback to the browser, or if requested,
529
  // return it raw, so calling code can do more processing.
530
  if ($deliver) {
531
    $default_delivery_callback = (isset($router_item) && $router_item) ? $router_item['delivery_callback'] : NULL;
532
    drupal_deliver_page($page_callback_result, $default_delivery_callback);
533
  }
534
  else {
535
    return $page_callback_result;
536
  }
537
}
538

    
539
/**
540
 * Loads objects into the map as defined in the $item['load_functions'].
541
 *
542
 * @param $item
543
 *   A menu router or menu link item
544
 * @param $map
545
 *   An array of path arguments; for example, array('node', '5').
546
 *
547
 * @return
548
 *   Returns TRUE for success, FALSE if an object cannot be loaded.
549
 *   Names of object loading functions are placed in $item['load_functions'].
550
 *   Loaded objects are placed in $map[]; keys are the same as keys in the
551
 *   $item['load_functions'] array.
552
 *   $item['access'] is set to FALSE if an object cannot be loaded.
553
 */
554
function _menu_load_objects(&$item, &$map) {
555
  if ($load_functions = $item['load_functions']) {
556
    // If someone calls this function twice, then unserialize will fail.
557
    if (!is_array($load_functions)) {
558
      $load_functions = unserialize($load_functions);
559
    }
560
    $path_map = $map;
561
    foreach ($load_functions as $index => $function) {
562
      if ($function) {
563
        $value = isset($path_map[$index]) ? $path_map[$index] : '';
564
        if (is_array($function)) {
565
          // Set up arguments for the load function. These were pulled from
566
          // 'load arguments' in the hook_menu() entry, but they need
567
          // some processing. In this case the $function is the key to the
568
          // load_function array, and the value is the list of arguments.
569
          list($function, $args) = each($function);
570
          $load_functions[$index] = $function;
571

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

    
609
/**
610
 * Checks access to a menu item using the access callback.
611
 *
612
 * @param $item
613
 *   A menu router or menu link item
614
 * @param $map
615
 *   An array of path arguments; for example, array('node', '5').
616
 *
617
 * @return
618
 *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
619
 */
620
function _menu_check_access(&$item, $map) {
621
  $item['access'] = FALSE;
622
  // Determine access callback, which will decide whether or not the current
623
  // user has access to this path.
624
  $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']);
625
  // Check for a TRUE or FALSE value.
626
  if (is_numeric($callback)) {
627
    $item['access'] = (bool) $callback;
628
  }
629
  else {
630
    $arguments = menu_unserialize($item['access_arguments'], $map);
631
    // As call_user_func_array is quite slow and user_access is a very common
632
    // callback, it is worth making a special case for it.
633
    if ($callback == 'user_access') {
634
      $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
635
    }
636
    elseif (function_exists($callback)) {
637
      $item['access'] = call_user_func_array($callback, $arguments);
638
    }
639
  }
640
}
641

    
642
/**
643
 * Localizes the router item title using t() or another callback.
644
 *
645
 * Translate the title and description to allow storage of English title
646
 * strings in the database, yet display of them in the language required
647
 * by the current user.
648
 *
649
 * @param $item
650
 *   A menu router item or a menu link item.
651
 * @param $map
652
 *   The path as an array with objects already replaced. E.g., for path
653
 *   node/123 $map would be array('node', $node) where $node is the node
654
 *   object for node 123.
655
 * @param $link_translate
656
 *   TRUE if we are translating a menu link item; FALSE if we are
657
 *   translating a menu router item.
658
 *
659
 * @return
660
 *   No return value.
661
 *   $item['title'] is localized according to $item['title_callback'].
662
 *   If an item's callback is check_plain(), $item['options']['html'] becomes
663
 *   TRUE.
664
 *   $item['description'] is translated using t().
665
 *   When doing link translation and the $item['options']['attributes']['title']
666
 *   (link title attribute) matches the description, it is translated as well.
667
 */
668
function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
669
  $callback = $item['title_callback'];
670
  $item['localized_options'] = $item['options'];
671
  // All 'class' attributes are assumed to be an array during rendering, but
672
  // links stored in the database may use an old string value.
673
  // @todo In order to remove this code we need to implement a database update
674
  //   including unserializing all existing link options and running this code
675
  //   on them, as well as adding validation to menu_link_save().
676
  if (isset($item['options']['attributes']['class']) && is_string($item['options']['attributes']['class'])) {
677
    $item['localized_options']['attributes']['class'] = explode(' ', $item['options']['attributes']['class']);
678
  }
679
  // If we are translating the title of a menu link, and its title is the same
680
  // as the corresponding router item, then we can use the title information
681
  // from the router. If it's customized, then we need to use the link title
682
  // itself; can't localize.
683
  // If we are translating a router item (tabs, page, breadcrumb), then we
684
  // can always use the information from the router item.
685
  if (!$link_translate || ($item['title'] == $item['link_title'])) {
686
    // t() is a special case. Since it is used very close to all the time,
687
    // we handle it directly instead of using indirect, slower methods.
688
    if ($callback == 't') {
689
      if (empty($item['title_arguments'])) {
690
        $item['title'] = t($item['title']);
691
      }
692
      else {
693
        $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
694
      }
695
    }
696
    elseif ($callback && function_exists($callback)) {
697
      if (empty($item['title_arguments'])) {
698
        $item['title'] = $callback($item['title']);
699
      }
700
      else {
701
        $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
702
      }
703
      // Avoid calling check_plain again on l() function.
704
      if ($callback == 'check_plain') {
705
        $item['localized_options']['html'] = TRUE;
706
      }
707
    }
708
  }
709
  elseif ($link_translate) {
710
    $item['title'] = $item['link_title'];
711
  }
712

    
713
  // Translate description, see the motivation above.
714
  if (!empty($item['description'])) {
715
    $original_description = $item['description'];
716
    $item['description'] = t($item['description']);
717
    if ($link_translate && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) {
718
      $item['localized_options']['attributes']['title'] = $item['description'];
719
    }
720
  }
721
}
722

    
723
/**
724
 * Handles dynamic path translation and menu access control.
725
 *
726
 * When a user arrives on a page such as node/5, this function determines
727
 * what "5" corresponds to, by inspecting the page's menu path definition,
728
 * node/%node. This will call node_load(5) to load the corresponding node
729
 * object.
730
 *
731
 * It also works in reverse, to allow the display of tabs and menu items which
732
 * contain these dynamic arguments, translating node/%node to node/5.
733
 *
734
 * Translation of menu item titles and descriptions are done here to
735
 * allow for storage of English strings in the database, and translation
736
 * to the language required to generate the current page.
737
 *
738
 * @param $router_item
739
 *   A menu router item
740
 * @param $map
741
 *   An array of path arguments; for example, array('node', '5').
742
 * @param $to_arg
743
 *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
744
 *   path from the menu table, for example tabs.
745
 *
746
 * @return
747
 *   Returns the map with objects loaded as defined in the
748
 *   $item['load_functions']. $item['access'] becomes TRUE if the item is
749
 *   accessible, FALSE otherwise. $item['href'] is set according to the map.
750
 *   If an error occurs during calling the load_functions (like trying to load
751
 *   a non-existent node) then this function returns FALSE.
752
 */
753
function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
754
  if ($to_arg && !empty($router_item['to_arg_functions'])) {
755
    // Fill in missing path elements, such as the current uid.
756
    _menu_link_map_translate($map, $router_item['to_arg_functions']);
757
  }
758
  // The $path_map saves the pieces of the path as strings, while elements in
759
  // $map may be replaced with loaded objects.
760
  $path_map = $map;
761
  if (!empty($router_item['load_functions']) && !_menu_load_objects($router_item, $map)) {
762
    // An error occurred loading an object.
763
    $router_item['access'] = FALSE;
764
    return FALSE;
765
  }
766

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

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

    
797
  return $map;
798
}
799

    
800
/**
801
 * Translates the path elements in the map using any to_arg helper function.
802
 *
803
 * @param $map
804
 *   An array of path arguments; for example, array('node', '5').
805
 * @param $to_arg_functions
806
 *   An array of helper functions; for example, array(2 => 'menu_tail_to_arg').
807
 *
808
 * @see hook_menu()
809
 */
810
function _menu_link_map_translate(&$map, $to_arg_functions) {
811
  $to_arg_functions = unserialize($to_arg_functions);
812
  foreach ($to_arg_functions as $index => $function) {
813
    // Translate place-holders into real values.
814
    $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
815
    if (!empty($map[$index]) || isset($arg)) {
816
      $map[$index] = $arg;
817
    }
818
    else {
819
      unset($map[$index]);
820
    }
821
  }
822
}
823

    
824
/**
825
 * Returns a string containing the path relative to the current index.
826
 */
827
function menu_tail_to_arg($arg, $map, $index) {
828
  return implode('/', array_slice($map, $index));
829
}
830

    
831
/**
832
 * Loads the path as one string relative to the current index.
833
 *
834
 * To use this load function, you must specify the load arguments
835
 * in the router item as:
836
 * @code
837
 * $item['load arguments'] = array('%map', '%index');
838
 * @endcode
839
 *
840
 * @see search_menu().
841
 */
842
function menu_tail_load($arg, &$map, $index) {
843
  $arg = implode('/', array_slice($map, $index));
844
  $map = array_slice($map, 0, $index);
845
  return $arg;
846
}
847

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

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

    
935
  // Allow other customizations - e.g. adding a page-specific query string to the
936
  // options array. For performance reasons we only invoke this hook if the link
937
  // has the 'alter' flag set in the options array.
938
  if (!empty($item['options']['alter'])) {
939
    drupal_alter('translated_menu_link', $item, $map);
940
  }
941

    
942
  return $map;
943
}
944

    
945
/**
946
 * Gets a loaded object from a router item.
947
 *
948
 * menu_get_object() provides access to objects loaded by the current router
949
 * item. For example, on the page node/%node, the router loads the %node object,
950
 * and calling menu_get_object() will return that. Normally, it is necessary to
951
 * specify the type of object referenced, however node is the default.
952
 * The following example tests to see whether the node being displayed is of the
953
 * "story" content type:
954
 * @code
955
 * $node = menu_get_object();
956
 * $story = $node->type == 'story';
957
 * @endcode
958
 *
959
 * @param $type
960
 *   Type of the object. These appear in hook_menu definitions as %type. Core
961
 *   provides aggregator_feed, aggregator_category, contact, filter_format,
962
 *   forum_term, menu, menu_link, node, taxonomy_vocabulary, user. See the
963
 *   relevant {$type}_load function for more on each. Defaults to node.
964
 * @param $position
965
 *   The position of the object in the path, where the first path segment is 0.
966
 *   For node/%node, the position of %node is 1, but for comment/reply/%node,
967
 *   it's 2. Defaults to 1.
968
 * @param $path
969
 *   See menu_get_item() for more on this. Defaults to the current path.
970
 */
971
function menu_get_object($type = 'node', $position = 1, $path = NULL) {
972
  $router_item = menu_get_item($path);
973
  if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type . '_load') {
974
    return $router_item['map'][$position];
975
  }
976
}
977

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

    
995
  if (!isset($menu_output[$menu_name])) {
996
    $tree = menu_tree_page_data($menu_name);
997
    $menu_output[$menu_name] = menu_tree_output($tree);
998
  }
999
  return $menu_output[$menu_name];
1000
}
1001

    
1002
/**
1003
 * Returns a rendered menu tree.
1004
 *
1005
 * The menu item's LI element is given one of the following classes:
1006
 * - expanded: The menu item is showing its submenu.
1007
 * - collapsed: The menu item has a submenu which is not shown.
1008
 * - leaf: The menu item has no submenu.
1009
 *
1010
 * @param $tree
1011
 *   A data structure representing the tree as returned from menu_tree_data.
1012
 *
1013
 * @return
1014
 *   A structured array to be rendered by drupal_render().
1015
 */
1016
function menu_tree_output($tree) {
1017
  $build = array();
1018
  $items = array();
1019

    
1020
  // Pull out just the menu links we are going to render so that we
1021
  // get an accurate count for the first/last classes.
1022
  foreach ($tree as $data) {
1023
    if ($data['link']['access'] && !$data['link']['hidden']) {
1024
      $items[] = $data;
1025
    }
1026
  }
1027

    
1028
  $router_item = menu_get_item();
1029
  $num_items = count($items);
1030
  foreach ($items as $i => $data) {
1031
    $class = array();
1032
    if ($i == 0) {
1033
      $class[] = 'first';
1034
    }
1035
    if ($i == $num_items - 1) {
1036
      $class[] = 'last';
1037
    }
1038
    // Set a class for the <li>-tag. Since $data['below'] may contain local
1039
    // tasks, only set 'expanded' class if the link also has children within
1040
    // the current menu.
1041
    if ($data['link']['has_children'] && $data['below']) {
1042
      $class[] = 'expanded';
1043
    }
1044
    elseif ($data['link']['has_children']) {
1045
      $class[] = 'collapsed';
1046
    }
1047
    else {
1048
      $class[] = 'leaf';
1049
    }
1050
    // Set a class if the link is in the active trail.
1051
    if ($data['link']['in_active_trail']) {
1052
      $class[] = 'active-trail';
1053
      $data['link']['localized_options']['attributes']['class'][] = 'active-trail';
1054
    }
1055
    // Normally, l() compares the href of every link with $_GET['q'] and sets
1056
    // the active class accordingly. But local tasks do not appear in menu
1057
    // trees, so if the current path is a local task, and this link is its
1058
    // tab root, then we have to set the class manually.
1059
    if ($data['link']['href'] == $router_item['tab_root_href'] && $data['link']['href'] != $_GET['q']) {
1060
      $data['link']['localized_options']['attributes']['class'][] = 'active';
1061
    }
1062

    
1063
    // Allow menu-specific theme overrides.
1064
    $element['#theme'] = 'menu_link__' . strtr($data['link']['menu_name'], '-', '_');
1065
    $element['#attributes']['class'] = $class;
1066
    $element['#title'] = $data['link']['title'];
1067
    $element['#href'] = $data['link']['href'];
1068
    $element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array();
1069
    $element['#below'] = $data['below'] ? menu_tree_output($data['below']) : $data['below'];
1070
    $element['#original_link'] = $data['link'];
1071
    // Index using the link's unique mlid.
1072
    $build[$data['link']['mlid']] = $element;
1073
  }
1074
  if ($build) {
1075
    // Make sure drupal_render() does not re-order the links.
1076
    $build['#sorted'] = TRUE;
1077
    // Add the theme wrapper for outer markup.
1078
    // Allow menu-specific theme overrides.
1079
    $build['#theme_wrappers'][] = 'menu_tree__' . strtr($data['link']['menu_name'], '-', '_');
1080
  }
1081

    
1082
  return $build;
1083
}
1084

    
1085
/**
1086
 * Gets the data structure representing a named menu tree.
1087
 *
1088
 * Since this can be the full tree including hidden items, the data returned
1089
 * may be used for generating an an admin interface or a select.
1090
 *
1091
 * @param $menu_name
1092
 *   The named menu links to return
1093
 * @param $link
1094
 *   A fully loaded menu link, or NULL. If a link is supplied, only the
1095
 *   path to root will be included in the returned tree - as if this link
1096
 *   represented the current page in a visible menu.
1097
 * @param $max_depth
1098
 *   Optional maximum depth of links to retrieve. Typically useful if only one
1099
 *   or two levels of a sub tree are needed in conjunction with a non-NULL
1100
 *   $link, in which case $max_depth should be greater than $link['depth'].
1101
 *
1102
 * @return
1103
 *   An tree of menu links in an array, in the order they should be rendered.
1104
 */
1105
function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) {
1106
  $tree = &drupal_static(__FUNCTION__, array());
1107

    
1108
  // Use $mlid as a flag for whether the data being loaded is for the whole tree.
1109
  $mlid = isset($link['mlid']) ? $link['mlid'] : 0;
1110
  // Generate a cache ID (cid) specific for this $menu_name, $link, $language, and depth.
1111
  $cid = 'links:' . $menu_name . ':all:' . $mlid . ':' . $GLOBALS['language']->language . ':' . (int) $max_depth;
1112

    
1113
  if (!isset($tree[$cid])) {
1114
    // If the static variable doesn't have the data, check {cache_menu}.
1115
    $cache = cache_get($cid, 'cache_menu');
1116
    if ($cache && isset($cache->data)) {
1117
      // If the cache entry exists, it contains the parameters for
1118
      // menu_build_tree().
1119
      $tree_parameters = $cache->data;
1120
    }
1121
    // If the tree data was not in the cache, build $tree_parameters.
1122
    if (!isset($tree_parameters)) {
1123
      $tree_parameters = array(
1124
        'min_depth' => 1,
1125
        'max_depth' => $max_depth,
1126
      );
1127
      if ($mlid) {
1128
        // The tree is for a single item, so we need to match the values in its
1129
        // p columns and 0 (the top level) with the plid values of other links.
1130
        $parents = array(0);
1131
        for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
1132
          if (!empty($link["p$i"])) {
1133
            $parents[] = $link["p$i"];
1134
          }
1135
        }
1136
        $tree_parameters['expanded'] = $parents;
1137
        $tree_parameters['active_trail'] = $parents;
1138
        $tree_parameters['active_trail'][] = $mlid;
1139
      }
1140

    
1141
      // Cache the tree building parameters using the page-specific cid.
1142
      cache_set($cid, $tree_parameters, 'cache_menu');
1143
    }
1144

    
1145
    // Build the tree using the parameters; the resulting tree will be cached
1146
    // by _menu_build_tree()).
1147
    $tree[$cid] = menu_build_tree($menu_name, $tree_parameters);
1148
  }
1149

    
1150
  return $tree[$cid];
1151
}
1152

    
1153
/**
1154
 * Sets the path for determining the active trail of the specified menu tree.
1155
 *
1156
 * This path will also affect the breadcrumbs under some circumstances.
1157
 * Breadcrumbs are built using the preferred link returned by
1158
 * menu_link_get_preferred(). If the preferred link is inside one of the menus
1159
 * specified in calls to menu_tree_set_path(), the preferred link will be
1160
 * overridden by the corresponding path returned by menu_tree_get_path().
1161
 *
1162
 * Setting this path does not affect the main content; for that use
1163
 * menu_set_active_item() instead.
1164
 *
1165
 * @param $menu_name
1166
 *   The name of the affected menu tree.
1167
 * @param $path
1168
 *   The path to use when finding the active trail.
1169
 */
1170
function menu_tree_set_path($menu_name, $path = NULL) {
1171
  $paths = &drupal_static(__FUNCTION__);
1172
  if (isset($path)) {
1173
    $paths[$menu_name] = $path;
1174
  }
1175
  return isset($paths[$menu_name]) ? $paths[$menu_name] : NULL;
1176
}
1177

    
1178
/**
1179
 * Gets the path for determining the active trail of the specified menu tree.
1180
 *
1181
 * @param $menu_name
1182
 *   The menu name of the requested tree.
1183
 *
1184
 * @return
1185
 *   A string containing the path. If no path has been specified with
1186
 *   menu_tree_set_path(), NULL is returned.
1187
 */
1188
function menu_tree_get_path($menu_name) {
1189
  return menu_tree_set_path($menu_name);
1190
}
1191

    
1192
/**
1193
 * Gets the data structure for a named menu tree, based on the current page.
1194
 *
1195
 * The tree order is maintained by storing each parent in an individual
1196
 * field, see http://drupal.org/node/141866 for more.
1197
 *
1198
 * @param $menu_name
1199
 *   The named menu links to return.
1200
 * @param $max_depth
1201
 *   (optional) The maximum depth of links to retrieve.
1202
 * @param $only_active_trail
1203
 *   (optional) Whether to only return the links in the active trail (TRUE)
1204
 *   instead of all links on every level of the menu link tree (FALSE). Defaults
1205
 *   to FALSE. Internally used for breadcrumbs only.
1206
 *
1207
 * @return
1208
 *   An array of menu links, in the order they should be rendered. The array
1209
 *   is a list of associative arrays -- these have two keys, link and below.
1210
 *   link is a menu item, ready for theming as a link. Below represents the
1211
 *   submenu below the link if there is one, and it is a subtree that has the
1212
 *   same structure described for the top-level array.
1213
 */
1214
function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail = FALSE) {
1215
  $tree = &drupal_static(__FUNCTION__, array());
1216

    
1217
  // Check if the active trail has been overridden for this menu tree.
1218
  $active_path = menu_tree_get_path($menu_name);
1219
  // Load the menu item corresponding to the current page.
1220
  if ($item = menu_get_item($active_path)) {
1221
    if (isset($max_depth)) {
1222
      $max_depth = min($max_depth, MENU_MAX_DEPTH);
1223
    }
1224
    // Generate a cache ID (cid) specific for this page.
1225
    $cid = 'links:' . $menu_name . ':page:' . $item['href'] . ':' . $GLOBALS['language']->language . ':' . (int) $item['access'] . ':' . (int) $max_depth;
1226
    // If we are asked for the active trail only, and $menu_name has not been
1227
    // built and cached for this page yet, then this likely means that it
1228
    // won't be built anymore, as this function is invoked from
1229
    // template_process_page(). So in order to not build a giant menu tree
1230
    // that needs to be checked for access on all levels, we simply check
1231
    // whether we have the menu already in cache, or otherwise, build a minimum
1232
    // tree containing the breadcrumb/active trail only.
1233
    // @see menu_set_active_trail()
1234
    if (!isset($tree[$cid]) && $only_active_trail) {
1235
      $cid .= ':trail';
1236
    }
1237

    
1238
    if (!isset($tree[$cid])) {
1239
      // If the static variable doesn't have the data, check {cache_menu}.
1240
      $cache = cache_get($cid, 'cache_menu');
1241
      if ($cache && isset($cache->data)) {
1242
        // If the cache entry exists, it contains the parameters for
1243
        // menu_build_tree().
1244
        $tree_parameters = $cache->data;
1245
      }
1246
      // If the tree data was not in the cache, build $tree_parameters.
1247
      if (!isset($tree_parameters)) {
1248
        $tree_parameters = array(
1249
          'min_depth' => 1,
1250
          'max_depth' => $max_depth,
1251
        );
1252
        // Parent mlids; used both as key and value to ensure uniqueness.
1253
        // We always want all the top-level links with plid == 0.
1254
        $active_trail = array(0 => 0);
1255

    
1256
        // If the item for the current page is accessible, build the tree
1257
        // parameters accordingly.
1258
        if ($item['access']) {
1259
          // Find a menu link corresponding to the current path. If $active_path
1260
          // is NULL, let menu_link_get_preferred() determine the path.
1261
          if ($active_link = menu_link_get_preferred($active_path, $menu_name)) {
1262
            // The active link may only be taken into account to build the
1263
            // active trail, if it resides in the requested menu. Otherwise,
1264
            // we'd needlessly re-run _menu_build_tree() queries for every menu
1265
            // on every page.
1266
            if ($active_link['menu_name'] == $menu_name) {
1267
              // Use all the coordinates, except the last one because there
1268
              // can be no child beyond the last column.
1269
              for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
1270
                if ($active_link['p' . $i]) {
1271
                  $active_trail[$active_link['p' . $i]] = $active_link['p' . $i];
1272
                }
1273
              }
1274
              // If we are asked to build links for the active trail only, skip
1275
              // the entire 'expanded' handling.
1276
              if ($only_active_trail) {
1277
                $tree_parameters['only_active_trail'] = TRUE;
1278
              }
1279
            }
1280
          }
1281
          $parents = $active_trail;
1282

    
1283
          $expanded = variable_get('menu_expanded', array());
1284
          // Check whether the current menu has any links set to be expanded.
1285
          if (!$only_active_trail && in_array($menu_name, $expanded)) {
1286
            // Collect all the links set to be expanded, and then add all of
1287
            // their children to the list as well.
1288
            do {
1289
              $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC))
1290
                ->fields('menu_links', array('mlid'))
1291
                ->condition('menu_name', $menu_name)
1292
                ->condition('expanded', 1)
1293
                ->condition('has_children', 1)
1294
                ->condition('plid', $parents, 'IN')
1295
                ->condition('mlid', $parents, 'NOT IN')
1296
                ->execute();
1297
              $num_rows = FALSE;
1298
              foreach ($result as $item) {
1299
                $parents[$item['mlid']] = $item['mlid'];
1300
                $num_rows = TRUE;
1301
              }
1302
            } while ($num_rows);
1303
          }
1304
          $tree_parameters['expanded'] = $parents;
1305
          $tree_parameters['active_trail'] = $active_trail;
1306
        }
1307
        // If access is denied, we only show top-level links in menus.
1308
        else {
1309
          $tree_parameters['expanded'] = $active_trail;
1310
          $tree_parameters['active_trail'] = $active_trail;
1311
        }
1312
        // Cache the tree building parameters using the page-specific cid.
1313
        cache_set($cid, $tree_parameters, 'cache_menu');
1314
      }
1315

    
1316
      // Build the tree using the parameters; the resulting tree will be cached
1317
      // by _menu_build_tree().
1318
      $tree[$cid] = menu_build_tree($menu_name, $tree_parameters);
1319
    }
1320
    return $tree[$cid];
1321
  }
1322

    
1323
  return array();
1324
}
1325

    
1326
/**
1327
 * Builds a menu tree, translates links, and checks access.
1328
 *
1329
 * @param $menu_name
1330
 *   The name of the menu.
1331
 * @param $parameters
1332
 *   (optional) An associative array of build parameters. Possible keys:
1333
 *   - expanded: An array of parent link ids to return only menu links that are
1334
 *     children of one of the plids in this list. If empty, the whole menu tree
1335
 *     is built, unless 'only_active_trail' is TRUE.
1336
 *   - active_trail: An array of mlids, representing the coordinates of the
1337
 *     currently active menu link.
1338
 *   - only_active_trail: Whether to only return links that are in the active
1339
 *     trail. This option is ignored, if 'expanded' is non-empty. Internally
1340
 *     used for breadcrumbs.
1341
 *   - min_depth: The minimum depth of menu links in the resulting tree.
1342
 *     Defaults to 1, which is the default to build a whole tree for a menu
1343
 *     (excluding menu container itself).
1344
 *   - max_depth: The maximum depth of menu links in the resulting tree.
1345
 *   - conditions: An associative array of custom database select query
1346
 *     condition key/value pairs; see _menu_build_tree() for the actual query.
1347
 *
1348
 * @return
1349
 *   A fully built menu tree.
1350
 */
1351
function menu_build_tree($menu_name, array $parameters = array()) {
1352
  // Build the menu tree.
1353
  $data = _menu_build_tree($menu_name, $parameters);
1354
  // Check access for the current user to each item in the tree.
1355
  menu_tree_check_access($data['tree'], $data['node_links']);
1356
  return $data['tree'];
1357
}
1358

    
1359
/**
1360
 * Builds a menu tree.
1361
 *
1362
 * This function may be used build the data for a menu tree only, for example
1363
 * to further massage the data manually before further processing happens.
1364
 * menu_tree_check_access() needs to be invoked afterwards.
1365
 *
1366
 * @see menu_build_tree()
1367
 */
1368
function _menu_build_tree($menu_name, array $parameters = array()) {
1369
  // Static cache of already built menu trees.
1370
  $trees = &drupal_static(__FUNCTION__, array());
1371

    
1372
  // Build the cache id; sort parents to prevent duplicate storage and remove
1373
  // default parameter values.
1374
  if (isset($parameters['expanded'])) {
1375
    sort($parameters['expanded']);
1376
  }
1377
  $tree_cid = 'links:' . $menu_name . ':tree-data:' . $GLOBALS['language']->language . ':' . hash('sha256', serialize($parameters));
1378

    
1379
  // If we do not have this tree in the static cache, check {cache_menu}.
1380
  if (!isset($trees[$tree_cid])) {
1381
    $cache = cache_get($tree_cid, 'cache_menu');
1382
    if ($cache && isset($cache->data)) {
1383
      $trees[$tree_cid] = $cache->data;
1384
    }
1385
  }
1386

    
1387
  if (!isset($trees[$tree_cid])) {
1388
    // Select the links from the table, and recursively build the tree. We
1389
    // LEFT JOIN since there is no match in {menu_router} for an external
1390
    // link.
1391
    $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
1392
    $query->addTag('translatable');
1393
    $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
1394
    $query->fields('ml');
1395
    $query->fields('m', array(
1396
      'load_functions',
1397
      'to_arg_functions',
1398
      'access_callback',
1399
      'access_arguments',
1400
      'page_callback',
1401
      'page_arguments',
1402
      'delivery_callback',
1403
      'tab_parent',
1404
      'tab_root',
1405
      'title',
1406
      'title_callback',
1407
      'title_arguments',
1408
      'theme_callback',
1409
      'theme_arguments',
1410
      'type',
1411
      'description',
1412
    ));
1413
    for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
1414
      $query->orderBy('p' . $i, 'ASC');
1415
    }
1416
    $query->condition('ml.menu_name', $menu_name);
1417
    if (!empty($parameters['expanded'])) {
1418
      $query->condition('ml.plid', $parameters['expanded'], 'IN');
1419
    }
1420
    elseif (!empty($parameters['only_active_trail'])) {
1421
      $query->condition('ml.mlid', $parameters['active_trail'], 'IN');
1422
    }
1423
    $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1);
1424
    if ($min_depth != 1) {
1425
      $query->condition('ml.depth', $min_depth, '>=');
1426
    }
1427
    if (isset($parameters['max_depth'])) {
1428
      $query->condition('ml.depth', $parameters['max_depth'], '<=');
1429
    }
1430
    // Add custom query conditions, if any were passed.
1431
    if (isset($parameters['conditions'])) {
1432
      foreach ($parameters['conditions'] as $column => $value) {
1433
        $query->condition($column, $value);
1434
      }
1435
    }
1436

    
1437
    // Build an ordered array of links using the query result object.
1438
    $links = array();
1439
    foreach ($query->execute() as $item) {
1440
      $links[] = $item;
1441
    }
1442
    $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array());
1443
    $data['tree'] = menu_tree_data($links, $active_trail, $min_depth);
1444
    $data['node_links'] = array();
1445
    menu_tree_collect_node_links($data['tree'], $data['node_links']);
1446

    
1447
    // Cache the data, if it is not already in the cache.
1448
    cache_set($tree_cid, $data, 'cache_menu');
1449
    $trees[$tree_cid] = $data;
1450
  }
1451

    
1452
  return $trees[$tree_cid];
1453
}
1454

    
1455
/**
1456
 * Collects node links from a given menu tree recursively.
1457
 *
1458
 * @param $tree
1459
 *   The menu tree you wish to collect node links from.
1460
 * @param $node_links
1461
 *   An array in which to store the collected node links.
1462
 */
1463
function menu_tree_collect_node_links(&$tree, &$node_links) {
1464
  foreach ($tree as $key => $v) {
1465
    if ($tree[$key]['link']['router_path'] == 'node/%') {
1466
      $nid = substr($tree[$key]['link']['link_path'], 5);
1467
      if (is_numeric($nid)) {
1468
        $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link'];
1469
        $tree[$key]['link']['access'] = FALSE;
1470
      }
1471
    }
1472
    if ($tree[$key]['below']) {
1473
      menu_tree_collect_node_links($tree[$key]['below'], $node_links);
1474
    }
1475
  }
1476
}
1477

    
1478
/**
1479
 * Checks access and performs dynamic operations for each link in the tree.
1480
 *
1481
 * @param $tree
1482
 *   The menu tree you wish to operate on.
1483
 * @param $node_links
1484
 *   A collection of node link references generated from $tree by
1485
 *   menu_tree_collect_node_links().
1486
 */
1487
function menu_tree_check_access(&$tree, $node_links = array()) {
1488
  if ($node_links) {
1489
    $nids = array_keys($node_links);
1490
    $select = db_select('node', 'n');
1491
    $select->addField('n', 'nid');
1492
    $select->condition('n.status', 1);
1493
    $select->condition('n.nid', $nids, 'IN');
1494
    $select->addTag('node_access');
1495
    $nids = $select->execute()->fetchCol();
1496
    foreach ($nids as $nid) {
1497
      foreach ($node_links[$nid] as $mlid => $link) {
1498
        $node_links[$nid][$mlid]['access'] = TRUE;
1499
      }
1500
    }
1501
  }
1502
  _menu_tree_check_access($tree);
1503
}
1504

    
1505
/**
1506
 * Sorts the menu tree and recursively checks access for each item.
1507
 */
1508
function _menu_tree_check_access(&$tree) {
1509
  $new_tree = array();
1510
  foreach ($tree as $key => $v) {
1511
    $item = &$tree[$key]['link'];
1512
    _menu_link_translate($item);
1513
    if ($item['access'] || ($item['in_active_trail'] && strpos($item['href'], '%') !== FALSE)) {
1514
      if ($tree[$key]['below']) {
1515
        _menu_tree_check_access($tree[$key]['below']);
1516
      }
1517
      // The weights are made a uniform 5 digits by adding 50000 as an offset.
1518
      // After _menu_link_translate(), $item['title'] has the localized link title.
1519
      // Adding the mlid to the end of the index insures that it is unique.
1520
      $new_tree[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $tree[$key];
1521
    }
1522
  }
1523
  // Sort siblings in the tree based on the weights and localized titles.
1524
  ksort($new_tree);
1525
  $tree = $new_tree;
1526
}
1527

    
1528
/**
1529
 * Sorts and returns the built data representing a menu tree.
1530
 *
1531
 * @param $links
1532
 *   A flat array of menu links that are part of the menu. Each array element
1533
 *   is an associative array of information about the menu link, containing the
1534
 *   fields from the {menu_links} table, and optionally additional information
1535
 *   from the {menu_router} table, if the menu item appears in both tables.
1536
 *   This array must be ordered depth-first. See _menu_build_tree() for a sample
1537
 *   query.
1538
 * @param $parents
1539
 *   An array of the menu link ID values that are in the path from the current
1540
 *   page to the root of the menu tree.
1541
 * @param $depth
1542
 *   The minimum depth to include in the returned menu tree.
1543
 *
1544
 * @return
1545
 *   An array of menu links in the form of a tree. Each item in the tree is an
1546
 *   associative array containing:
1547
 *   - link: The menu link item from $links, with additional element
1548
 *     'in_active_trail' (TRUE if the link ID was in $parents).
1549
 *   - below: An array containing the sub-tree of this item, where each element
1550
 *     is a tree item array with 'link' and 'below' elements. This array will be
1551
 *     empty if the menu item has no items in its sub-tree having a depth
1552
 *     greater than or equal to $depth.
1553
 */
1554
function menu_tree_data(array $links, array $parents = array(), $depth = 1) {
1555
  // Reverse the array so we can use the more efficient array_pop() function.
1556
  $links = array_reverse($links);
1557
  return _menu_tree_data($links, $parents, $depth);
1558
}
1559

    
1560
/**
1561
 * Builds the data representing a menu tree.
1562
 *
1563
 * The function is a bit complex because the rendering of a link depends on
1564
 * the next menu link.
1565
 */
1566
function _menu_tree_data(&$links, $parents, $depth) {
1567
  $tree = array();
1568
  while ($item = array_pop($links)) {
1569
    // We need to determine if we're on the path to root so we can later build
1570
    // the correct active trail and breadcrumb.
1571
    $item['in_active_trail'] = in_array($item['mlid'], $parents);
1572
    // Add the current link to the tree.
1573
    $tree[$item['mlid']] = array(
1574
      'link' => $item,
1575
      'below' => array(),
1576
    );
1577
    // Look ahead to the next link, but leave it on the array so it's available
1578
    // to other recursive function calls if we return or build a sub-tree.
1579
    $next = end($links);
1580
    // Check whether the next link is the first in a new sub-tree.
1581
    if ($next && $next['depth'] > $depth) {
1582
      // Recursively call _menu_tree_data to build the sub-tree.
1583
      $tree[$item['mlid']]['below'] = _menu_tree_data($links, $parents, $next['depth']);
1584
      // Fetch next link after filling the sub-tree.
1585
      $next = end($links);
1586
    }
1587
    // Determine if we should exit the loop and return.
1588
    if (!$next || $next['depth'] < $depth) {
1589
      break;
1590
    }
1591
  }
1592
  return $tree;
1593
}
1594

    
1595
/**
1596
 * Implements template_preprocess_HOOK() for theme_menu_tree().
1597
 */
1598
function template_preprocess_menu_tree(&$variables) {
1599
  $variables['tree'] = $variables['tree']['#children'];
1600
}
1601

    
1602
/**
1603
 * Returns HTML for a wrapper for a menu sub-tree.
1604
 *
1605
 * @param $variables
1606
 *   An associative array containing:
1607
 *   - tree: An HTML string containing the tree's items.
1608
 *
1609
 * @see template_preprocess_menu_tree()
1610
 * @ingroup themeable
1611
 */
1612
function theme_menu_tree($variables) {
1613
  return '<ul class="menu">' . $variables['tree'] . '</ul>';
1614
}
1615

    
1616
/**
1617
 * Returns HTML for a menu link and submenu.
1618
 *
1619
 * @param $variables
1620
 *   An associative array containing:
1621
 *   - element: Structured array data for a menu link.
1622
 *
1623
 * @ingroup themeable
1624
 */
1625
function theme_menu_link(array $variables) {
1626
  $element = $variables['element'];
1627
  $sub_menu = '';
1628

    
1629
  if ($element['#below']) {
1630
    $sub_menu = drupal_render($element['#below']);
1631
  }
1632
  $output = l($element['#title'], $element['#href'], $element['#localized_options']);
1633
  return '<li' . drupal_attributes($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
1634
}
1635

    
1636
/**
1637
 * Returns HTML for a single local task link.
1638
 *
1639
 * @param $variables
1640
 *   An associative array containing:
1641
 *   - element: A render element containing:
1642
 *     - #link: A menu link array with 'title', 'href', and 'localized_options'
1643
 *       keys.
1644
 *     - #active: A boolean indicating whether the local task is active.
1645
 *
1646
 * @ingroup themeable
1647
 */
1648
function theme_menu_local_task($variables) {
1649
  $link = $variables['element']['#link'];
1650
  $link_text = $link['title'];
1651

    
1652
  if (!empty($variables['element']['#active'])) {
1653
    // Add text to indicate active tab for non-visual users.
1654
    $active = '<span class="element-invisible">' . t('(active tab)') . '</span>';
1655

    
1656
    // If the link does not contain HTML already, check_plain() it now.
1657
    // After we set 'html'=TRUE the link will not be sanitized by l().
1658
    if (empty($link['localized_options']['html'])) {
1659
      $link['title'] = check_plain($link['title']);
1660
    }
1661
    $link['localized_options']['html'] = TRUE;
1662
    $link_text = t('!local-task-title!active', array('!local-task-title' => $link['title'], '!active' => $active));
1663
  }
1664

    
1665
  return '<li' . (!empty($variables['element']['#active']) ? ' class="active"' : '') . '>' . l($link_text, $link['href'], $link['localized_options']) . "</li>\n";
1666
}
1667

    
1668
/**
1669
 * Returns HTML for a single local action link.
1670
 *
1671
 * @param $variables
1672
 *   An associative array containing:
1673
 *   - element: A render element containing:
1674
 *     - #link: A menu link array with 'title', 'href', and 'localized_options'
1675
 *       keys.
1676
 *
1677
 * @ingroup themeable
1678
 */
1679
function theme_menu_local_action($variables) {
1680
  $link = $variables['element']['#link'];
1681

    
1682
  $output = '<li>';
1683
  if (isset($link['href'])) {
1684
    $output .= l($link['title'], $link['href'], isset($link['localized_options']) ? $link['localized_options'] : array());
1685
  }
1686
  elseif (!empty($link['localized_options']['html'])) {
1687
    $output .= $link['title'];
1688
  }
1689
  else {
1690
    $output .= check_plain($link['title']);
1691
  }
1692
  $output .= "</li>\n";
1693

    
1694
  return $output;
1695
}
1696

    
1697
/**
1698
 * Generates elements for the $arg array in the help hook.
1699
 */
1700
function drupal_help_arg($arg = array()) {
1701
  // Note - the number of empty elements should be > MENU_MAX_PARTS.
1702
  return $arg + array('', '', '', '', '', '', '', '', '', '', '', '');
1703
}
1704

    
1705
/**
1706
 * Returns the help associated with the active menu item.
1707
 */
1708
function menu_get_active_help() {
1709
  $output = '';
1710
  $router_path = menu_tab_root_path();
1711
  // We will always have a path unless we are on a 403 or 404.
1712
  if (!$router_path) {
1713
    return '';
1714
  }
1715

    
1716
  $arg = drupal_help_arg(arg(NULL));
1717

    
1718
  foreach (module_implements('help') as $module) {
1719
    $function = $module . '_help';
1720
    // Lookup help for this path.
1721
    if ($help = $function($router_path, $arg)) {
1722
      $output .= $help . "\n";
1723
    }
1724
  }
1725
  return $output;
1726
}
1727

    
1728
/**
1729
 * Gets the custom theme for the current page, if there is one.
1730
 *
1731
 * @param $initialize
1732
 *   This parameter should only be used internally; it is set to TRUE in order
1733
 *   to force the custom theme to be initialized for the current page request.
1734
 *
1735
 * @return
1736
 *   The machine-readable name of the custom theme, if there is one.
1737
 *
1738
 * @see menu_set_custom_theme()
1739
 */
1740
function menu_get_custom_theme($initialize = FALSE) {
1741
  $custom_theme = &drupal_static(__FUNCTION__);
1742
  // Skip this if the site is offline or being installed or updated, since the
1743
  // menu system may not be correctly initialized then.
1744
  if ($initialize && !_menu_site_is_offline(TRUE) && (!defined('MAINTENANCE_MODE') || (MAINTENANCE_MODE != 'update' && MAINTENANCE_MODE != 'install'))) {
1745
    // First allow modules to dynamically set a custom theme for the current
1746
    // page. Since we can only have one, the last module to return a valid
1747
    // theme takes precedence.
1748
    $custom_themes = array_filter(module_invoke_all('custom_theme'), 'drupal_theme_access');
1749
    if (!empty($custom_themes)) {
1750
      $custom_theme = array_pop($custom_themes);
1751
    }
1752
    // If there is a theme callback function for the current page, execute it.
1753
    // If this returns a valid theme, it will override any theme that was set
1754
    // by a hook_custom_theme() implementation above.
1755
    $router_item = menu_get_item();
1756
    if (!empty($router_item['access']) && !empty($router_item['theme_callback']) && function_exists($router_item['theme_callback'])) {
1757
      $theme_name = call_user_func_array($router_item['theme_callback'], $router_item['theme_arguments']);
1758
      if (drupal_theme_access($theme_name)) {
1759
        $custom_theme = $theme_name;
1760
      }
1761
    }
1762
  }
1763
  return $custom_theme;
1764
}
1765

    
1766
/**
1767
 * Sets a custom theme for the current page, if there is one.
1768
 */
1769
function menu_set_custom_theme() {
1770
  menu_get_custom_theme(TRUE);
1771
}
1772

    
1773
/**
1774
 * Build a list of named menus.
1775
 */
1776
function menu_get_names() {
1777
  $names = &drupal_static(__FUNCTION__);
1778

    
1779
  if (empty($names)) {
1780
    $names = db_select('menu_links')
1781
      ->distinct()
1782
      ->fields('menu_links', array('menu_name'))
1783
      ->orderBy('menu_name')
1784
      ->execute()->fetchCol();
1785
  }
1786
  return $names;
1787
}
1788

    
1789
/**
1790
 * Returns an array containing the names of system-defined (default) menus.
1791
 */
1792
function menu_list_system_menus() {
1793
  return array(
1794
    'navigation' => 'Navigation',
1795
    'management' => 'Management',
1796
    'user-menu' => 'User menu',
1797
    'main-menu' => 'Main menu',
1798
  );
1799
}
1800

    
1801
/**
1802
 * Returns an array of links to be rendered as the Main menu.
1803
 */
1804
function menu_main_menu() {
1805
  return menu_navigation_links(variable_get('menu_main_links_source', 'main-menu'));
1806
}
1807

    
1808
/**
1809
 * Returns an array of links to be rendered as the Secondary links.
1810
 */
1811
function menu_secondary_menu() {
1812

    
1813
  // If the secondary menu source is set as the primary menu, we display the
1814
  // second level of the primary menu.
1815
  if (variable_get('menu_secondary_links_source', 'user-menu') == variable_get('menu_main_links_source', 'main-menu')) {
1816
    return menu_navigation_links(variable_get('menu_main_links_source', 'main-menu'), 1);
1817
  }
1818
  else {
1819
    return menu_navigation_links(variable_get('menu_secondary_links_source', 'user-menu'), 0);
1820
  }
1821
}
1822

    
1823
/**
1824
 * Returns an array of links for a navigation menu.
1825
 *
1826
 * @param $menu_name
1827
 *   The name of the menu.
1828
 * @param $level
1829
 *   Optional, the depth of the menu to be returned.
1830
 *
1831
 * @return
1832
 *   An array of links of the specified menu and level.
1833
 */
1834
function menu_navigation_links($menu_name, $level = 0) {
1835
  // Don't even bother querying the menu table if no menu is specified.
1836
  if (empty($menu_name)) {
1837
    return array();
1838
  }
1839

    
1840
  // Get the menu hierarchy for the current page.
1841
  $tree = menu_tree_page_data($menu_name, $level + 1);
1842

    
1843
  // Go down the active trail until the right level is reached.
1844
  while ($level-- > 0 && $tree) {
1845
    // Loop through the current level's items until we find one that is in trail.
1846
    while ($item = array_shift($tree)) {
1847
      if ($item['link']['in_active_trail']) {
1848
        // If the item is in the active trail, we continue in the subtree.
1849
        $tree = empty($item['below']) ? array() : $item['below'];
1850
        break;
1851
      }
1852
    }
1853
  }
1854

    
1855
  // Create a single level of links.
1856
  $router_item = menu_get_item();
1857
  $links = array();
1858
  foreach ($tree as $item) {
1859
    if (!$item['link']['hidden']) {
1860
      $class = '';
1861
      $l = $item['link']['localized_options'];
1862
      $l['href'] = $item['link']['href'];
1863
      $l['title'] = $item['link']['title'];
1864
      if ($item['link']['in_active_trail']) {
1865
        $class = ' active-trail';
1866
        $l['attributes']['class'][] = 'active-trail';
1867
      }
1868
      // Normally, l() compares the href of every link with $_GET['q'] and sets
1869
      // the active class accordingly. But local tasks do not appear in menu
1870
      // trees, so if the current path is a local task, and this link is its
1871
      // tab root, then we have to set the class manually.
1872
      if ($item['link']['href'] == $router_item['tab_root_href'] && $item['link']['href'] != $_GET['q']) {
1873
        $l['attributes']['class'][] = 'active';
1874
      }
1875
      // Keyed with the unique mlid to generate classes in theme_links().
1876
      $links['menu-' . $item['link']['mlid'] . $class] = $l;
1877
    }
1878
  }
1879
  return $links;
1880
}
1881

    
1882
/**
1883
 * Collects the local tasks (tabs), action links, and the root path.
1884
 *
1885
 * @param $level
1886
 *   The level of tasks you ask for. Primary tasks are 0, secondary are 1.
1887
 *
1888
 * @return
1889
 *   An array containing
1890
 *   - tabs: Local tasks for the requested level:
1891
 *     - count: The number of local tasks.
1892
 *     - output: The themed output of local tasks.
1893
 *   - actions: Action links for the requested level:
1894
 *     - count: The number of action links.
1895
 *     - output: The themed output of action links.
1896
 *   - root_path: The router path for the current page. If the current page is
1897
 *     a default local task, then this corresponds to the parent tab.
1898
 */
1899
function menu_local_tasks($level = 0) {
1900
  $data = &drupal_static(__FUNCTION__);
1901
  $root_path = &drupal_static(__FUNCTION__ . ':root_path', '');
1902
  $empty = array(
1903
    'tabs' => array('count' => 0, 'output' => array()),
1904
    'actions' => array('count' => 0, 'output' => array()),
1905
    'root_path' => &$root_path,
1906
  );
1907

    
1908
  if (!isset($data)) {
1909
    $data = array();
1910
    // Set defaults in case there are no actions or tabs.
1911
    $actions = $empty['actions'];
1912
    $tabs = array();
1913

    
1914
    $router_item = menu_get_item();
1915

    
1916
    // If this router item points to its parent, start from the parents to
1917
    // compute tabs and actions.
1918
    if ($router_item && ($router_item['type'] & MENU_LINKS_TO_PARENT)) {
1919
      $router_item = menu_get_item($router_item['tab_parent_href']);
1920
    }
1921

    
1922
    // If we failed to fetch a router item or the current user doesn't have
1923
    // access to it, don't bother computing the tabs.
1924
    if (!$router_item || !$router_item['access']) {
1925
      return $empty;
1926
    }
1927

    
1928
    // Get all tabs (also known as local tasks) and the root page.
1929
    $cid = 'local_tasks:' . $router_item['tab_root'];
1930
    if ($cache = cache_get($cid, 'cache_menu')) {
1931
      $result = $cache->data;
1932
    }
1933
    else {
1934
      $result = db_select('menu_router', NULL, array('fetch' => PDO::FETCH_ASSOC))
1935
        ->fields('menu_router')
1936
        ->condition('tab_root', $router_item['tab_root'])
1937
        ->condition('context', MENU_CONTEXT_INLINE, '<>')
1938
        ->orderBy('weight')
1939
        ->orderBy('title')
1940
        ->execute()
1941
        ->fetchAll();
1942
      cache_set($cid, $result, 'cache_menu');
1943
    }
1944
    $map = $router_item['original_map'];
1945
    $children = array();
1946
    $tasks = array();
1947
    $root_path = $router_item['path'];
1948

    
1949
    foreach ($result as $item) {
1950
      _menu_translate($item, $map, TRUE);
1951
      if ($item['tab_parent']) {
1952
        // All tabs, but not the root page.
1953
        $children[$item['tab_parent']][$item['path']] = $item;
1954
      }
1955
      // Store the translated item for later use.
1956
      $tasks[$item['path']] = $item;
1957
    }
1958

    
1959
    // Find all tabs below the current path.
1960
    $path = $router_item['path'];
1961
    // Tab parenting may skip levels, so the number of parts in the path may not
1962
    // equal the depth. Thus we use the $depth counter (offset by 1000 for ksort).
1963
    $depth = 1001;
1964
    $actions['count'] = 0;
1965
    $actions['output'] = array();
1966
    while (isset($children[$path])) {
1967
      $tabs_current = array();
1968
      $actions_current = array();
1969
      $next_path = '';
1970
      $tab_count = 0;
1971
      $action_count = 0;
1972
      foreach ($children[$path] as $item) {
1973
        // Local tasks can be normal items too, so bitmask with
1974
        // MENU_IS_LOCAL_TASK before checking.
1975
        if (!($item['type'] & MENU_IS_LOCAL_TASK)) {
1976
          // This item is not a tab, skip it.
1977
          continue;
1978
        }
1979
        if ($item['access']) {
1980
          $link = $item;
1981
          // The default task is always active. As tabs can be normal items
1982
          // too, so bitmask with MENU_LINKS_TO_PARENT before checking.
1983
          if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) {
1984
            // Find the first parent which is not a default local task or action.
1985
            for ($p = $item['tab_parent']; ($tasks[$p]['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT; $p = $tasks[$p]['tab_parent']);
1986
            // Use the path of the parent instead.
1987
            $link['href'] = $tasks[$p]['href'];
1988
            // Mark the link as active, if the current path happens to be the
1989
            // path of the default local task itself (i.e., instead of its
1990
            // tab_parent_href or tab_root_href). Normally, links for default
1991
            // local tasks link to their parent, but the path of default local
1992
            // tasks can still be accessed directly, in which case this link
1993
            // would not be marked as active, since l() only compares the href
1994
            // with $_GET['q'].
1995
            if ($link['href'] != $_GET['q']) {
1996
              $link['localized_options']['attributes']['class'][] = 'active';
1997
            }
1998
            $tabs_current[] = array(
1999
              '#theme' => 'menu_local_task',
2000
              '#link' => $link,
2001
              '#active' => TRUE,
2002
            );
2003
            $next_path = $item['path'];
2004
            $tab_count++;
2005
          }
2006
          else {
2007
            // Actions can be normal items too, so bitmask with
2008
            // MENU_IS_LOCAL_ACTION before checking.
2009
            if (($item['type'] & MENU_IS_LOCAL_ACTION) == MENU_IS_LOCAL_ACTION) {
2010
              // The item is an action, display it as such.
2011
              $actions_current[] = array(
2012
                '#theme' => 'menu_local_action',
2013
                '#link' => $link,
2014
              );
2015
              $action_count++;
2016
            }
2017
            else {
2018
              // Otherwise, it's a normal tab.
2019
              $tabs_current[] = array(
2020
                '#theme' => 'menu_local_task',
2021
                '#link' => $link,
2022
              );
2023
              $tab_count++;
2024
            }
2025
          }
2026
        }
2027
      }
2028
      $path = $next_path;
2029
      $tabs[$depth]['count'] = $tab_count;
2030
      $tabs[$depth]['output'] = $tabs_current;
2031
      $actions['count'] += $action_count;
2032
      $actions['output'] = array_merge($actions['output'], $actions_current);
2033
      $depth++;
2034
    }
2035
    $data['actions'] = $actions;
2036
    // Find all tabs at the same level or above the current one.
2037
    $parent = $router_item['tab_parent'];
2038
    $path = $router_item['path'];
2039
    $current = $router_item;
2040
    $depth = 1000;
2041
    while (isset($children[$parent])) {
2042
      $tabs_current = array();
2043
      $next_path = '';
2044
      $next_parent = '';
2045
      $count = 0;
2046
      foreach ($children[$parent] as $item) {
2047
        // Skip local actions.
2048
        if ($item['type'] & MENU_IS_LOCAL_ACTION) {
2049
          continue;
2050
        }
2051
        if ($item['access']) {
2052
          $count++;
2053
          $link = $item;
2054
          // Local tasks can be normal items too, so bitmask with
2055
          // MENU_LINKS_TO_PARENT before checking.
2056
          if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) {
2057
            // Find the first parent which is not a default local task.
2058
            for ($p = $item['tab_parent']; ($tasks[$p]['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT; $p = $tasks[$p]['tab_parent']);
2059
            // Use the path of the parent instead.
2060
            $link['href'] = $tasks[$p]['href'];
2061
            if ($item['path'] == $router_item['path']) {
2062
              $root_path = $tasks[$p]['path'];
2063
            }
2064
          }
2065
          // We check for the active tab.
2066
          if ($item['path'] == $path) {
2067
            // Mark the link as active, if the current path is a (second-level)
2068
            // local task of a default local task. Since this default local task
2069
            // links to its parent, l() will not mark it as active, as it only
2070
            // compares the link's href to $_GET['q'].
2071
            if ($link['href'] != $_GET['q']) {
2072
              $link['localized_options']['attributes']['class'][] = 'active';
2073
            }
2074
            $tabs_current[] = array(
2075
              '#theme' => 'menu_local_task',
2076
              '#link' => $link,
2077
              '#active' => TRUE,
2078
            );
2079
            $next_path = $item['tab_parent'];
2080
            if (isset($tasks[$next_path])) {
2081
              $next_parent = $tasks[$next_path]['tab_parent'];
2082
            }
2083
          }
2084
          else {
2085
            $tabs_current[] = array(
2086
              '#theme' => 'menu_local_task',
2087
              '#link' => $link,
2088
            );
2089
          }
2090
        }
2091
      }
2092
      $path = $next_path;
2093
      $parent = $next_parent;
2094
      $tabs[$depth]['count'] = $count;
2095
      $tabs[$depth]['output'] = $tabs_current;
2096
      $depth--;
2097
    }
2098
    // Sort by depth.
2099
    ksort($tabs);
2100
    // Remove the depth, we are interested only in their relative placement.
2101
    $tabs = array_values($tabs);
2102
    $data['tabs'] = $tabs;
2103

    
2104
    // Allow modules to alter local tasks or dynamically append further tasks.
2105
    drupal_alter('menu_local_tasks', $data, $router_item, $root_path);
2106
  }
2107

    
2108
  if (isset($data['tabs'][$level])) {
2109
    return array(
2110
      'tabs' => $data['tabs'][$level],
2111
      'actions' => $data['actions'],
2112
      'root_path' => $root_path,
2113
    );
2114
  }
2115
  // @todo If there are no tabs, then there still can be actions; for example,
2116
  //   when added via hook_menu_local_tasks_alter().
2117
  elseif (!empty($data['actions']['output'])) {
2118
    return array('actions' => $data['actions']) + $empty;
2119
  }
2120
  return $empty;
2121
}
2122

    
2123
/**
2124
 * Retrieves contextual links for a path based on registered local tasks.
2125
 *
2126
 * This leverages the menu system to retrieve the first layer of registered
2127
 * local tasks for a given system path. All local tasks of the tab type
2128
 * MENU_CONTEXT_INLINE are taken into account.
2129
 *
2130
 * For example, when considering the following registered local tasks:
2131
 * - node/%node/view (default local task) with no 'context' defined
2132
 * - node/%node/edit with context: MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE
2133
 * - node/%node/revisions with context: MENU_CONTEXT_PAGE
2134
 * - node/%node/report-as-spam with context: MENU_CONTEXT_INLINE
2135
 *
2136
 * If the path "node/123" is passed to this function, then it will return the
2137
 * links for 'edit' and 'report-as-spam'.
2138
 *
2139
 * @param $module
2140
 *   The name of the implementing module. This is used to prefix the key for
2141
 *   each contextual link, which is transformed into a CSS class during
2142
 *   rendering by theme_links(). For example, if $module is 'block' and the
2143
 *   retrieved local task path argument is 'edit', then the resulting CSS class
2144
 *   will be 'block-edit'.
2145
 * @param $parent_path
2146
 *   The static menu router path of the object to retrieve local tasks for, for
2147
 *   example 'node' or 'admin/structure/block/manage'.
2148
 * @param $args
2149
 *   A list of dynamic path arguments to append to $parent_path to form the
2150
 *   fully-qualified menu router path; for example, array(123) for a certain
2151
 *   node or array('system', 'navigation') for a certain block.
2152
 *
2153
 * @return
2154
 *   A list of menu router items that are local tasks for the passed-in path.
2155
 *
2156
 * @see contextual_links_preprocess()
2157
 * @see hook_menu()
2158
 */
2159
function menu_contextual_links($module, $parent_path, $args) {
2160
  static $path_empty = array();
2161

    
2162
  $links = array();
2163
  // Performance: In case a previous invocation for the same parent path did not
2164
  // return any links, we immediately return here.
2165
  if (isset($path_empty[$parent_path]) && strpos($parent_path, '%') !== FALSE) {
2166
    return $links;
2167
  }
2168
  // Construct the item-specific parent path.
2169
  $path = $parent_path . '/' . implode('/', $args);
2170

    
2171
  // Get the router item for the given parent link path.
2172
  $router_item = menu_get_item($path);
2173
  if (!$router_item || !$router_item['access']) {
2174
    $path_empty[$parent_path] = TRUE;
2175
    return $links;
2176
  }
2177
  $data = &drupal_static(__FUNCTION__, array());
2178
  $root_path = $router_item['path'];
2179

    
2180
  // Performance: For a single, normalized path (such as 'node/%') we only query
2181
  // available tasks once per request.
2182
  if (!isset($data[$root_path])) {
2183
    // Get all contextual links that are direct children of the router item and
2184
    // not of the tab type 'view'.
2185
    $data[$root_path] = db_select('menu_router', 'm')
2186
      ->fields('m')
2187
      ->condition('tab_parent', $router_item['tab_root'])
2188
      ->condition('context', MENU_CONTEXT_NONE, '<>')
2189
      ->condition('context', MENU_CONTEXT_PAGE, '<>')
2190
      ->orderBy('weight')
2191
      ->orderBy('title')
2192
      ->execute()
2193
      ->fetchAllAssoc('path', PDO::FETCH_ASSOC);
2194
  }
2195
  $parent_length = drupal_strlen($root_path) + 1;
2196
  $map = $router_item['original_map'];
2197
  foreach ($data[$root_path] as $item) {
2198
    // Extract the actual "task" string from the path argument.
2199
    $key = drupal_substr($item['path'], $parent_length);
2200

    
2201
    // Denormalize and translate the contextual link.
2202
    _menu_translate($item, $map, TRUE);
2203
    if (!$item['access']) {
2204
      continue;
2205
    }
2206
    // All contextual links are keyed by the actual "task" path argument,
2207
    // prefixed with the name of the implementing module.
2208
    $links[$module . '-' . $key] = $item;
2209
  }
2210

    
2211
  // Allow modules to alter contextual links.
2212
  drupal_alter('menu_contextual_links', $links, $router_item, $root_path);
2213

    
2214
  // Performance: If the current user does not have access to any links for this
2215
  // router path and no other module added further links, we assign FALSE here
2216
  // to skip the entire process the next time the same router path is requested.
2217
  if (empty($links)) {
2218
    $path_empty[$parent_path] = TRUE;
2219
  }
2220

    
2221
  return $links;
2222
}
2223

    
2224
/**
2225
 * Returns the rendered local tasks at the top level.
2226
 */
2227
function menu_primary_local_tasks() {
2228
  $links = menu_local_tasks(0);
2229
  // Do not display single tabs.
2230
  return ($links['tabs']['count'] > 1 ? $links['tabs']['output'] : '');
2231
}
2232

    
2233
/**
2234
 * Returns the rendered local tasks at the second level.
2235
 */
2236
function menu_secondary_local_tasks() {
2237
  $links = menu_local_tasks(1);
2238
  // Do not display single tabs.
2239
  return ($links['tabs']['count'] > 1 ? $links['tabs']['output'] : '');
2240
}
2241

    
2242
/**
2243
 * Returns the rendered local actions at the current level.
2244
 */
2245
function menu_local_actions() {
2246
  $links = menu_local_tasks();
2247
  return $links['actions']['output'];
2248
}
2249

    
2250
/**
2251
 * Returns the router path, or the path for a default local task's parent.
2252
 */
2253
function menu_tab_root_path() {
2254
  $links = menu_local_tasks();
2255
  return $links['root_path'];
2256
}
2257

    
2258
/**
2259
 * Returns a renderable element for the primary and secondary tabs.
2260
 */
2261
function menu_local_tabs() {
2262
  return array(
2263
    '#theme' => 'menu_local_tasks',
2264
    '#primary' => menu_primary_local_tasks(),
2265
    '#secondary' => menu_secondary_local_tasks(),
2266
  );
2267
}
2268

    
2269
/**
2270
 * Returns HTML for primary and secondary local tasks.
2271
 *
2272
 * @param $variables
2273
 *   An associative array containing:
2274
 *     - primary: (optional) An array of local tasks (tabs).
2275
 *     - secondary: (optional) An array of local tasks (tabs).
2276
 *
2277
 * @ingroup themeable
2278
 * @see menu_local_tasks()
2279
 */
2280
function theme_menu_local_tasks(&$variables) {
2281
  $output = '';
2282

    
2283
  if (!empty($variables['primary'])) {
2284
    $variables['primary']['#prefix'] = '<h2 class="element-invisible">' . t('Primary tabs') . '</h2>';
2285
    $variables['primary']['#prefix'] .= '<ul class="tabs primary">';
2286
    $variables['primary']['#suffix'] = '</ul>';
2287
    $output .= drupal_render($variables['primary']);
2288
  }
2289
  if (!empty($variables['secondary'])) {
2290
    $variables['secondary']['#prefix'] = '<h2 class="element-invisible">' . t('Secondary tabs') . '</h2>';
2291
    $variables['secondary']['#prefix'] .= '<ul class="tabs secondary">';
2292
    $variables['secondary']['#suffix'] = '</ul>';
2293
    $output .= drupal_render($variables['secondary']);
2294
  }
2295

    
2296
  return $output;
2297
}
2298

    
2299
/**
2300
 * Sets (or gets) the active menu for the current page.
2301
 *
2302
 * The active menu for the page determines the active trail.
2303
 *
2304
 * @return
2305
 *   An array of menu machine names, in order of preference. The
2306
 *   'menu_default_active_menus' variable may be used to assert a menu order
2307
 *   different from the order of creation, or to prevent a particular menu from
2308
 *   being used at all in the active trail.
2309
 *   E.g., $conf['menu_default_active_menus'] = array('navigation', 'main-menu')
2310
 */
2311
function menu_set_active_menu_names($menu_names = NULL) {
2312
  $active = &drupal_static(__FUNCTION__);
2313

    
2314
  if (isset($menu_names) && is_array($menu_names)) {
2315
    $active = $menu_names;
2316
  }
2317
  elseif (!isset($active)) {
2318
    $active = variable_get('menu_default_active_menus', array_keys(menu_list_system_menus()));
2319
  }
2320
  return $active;
2321
}
2322

    
2323
/**
2324
 * Gets the active menu for the current page.
2325
 */
2326
function menu_get_active_menu_names() {
2327
  return menu_set_active_menu_names();
2328
}
2329

    
2330
/**
2331
 * Sets the active path, which determines which page is loaded.
2332
 *
2333
 * Note that this may not have the desired effect unless invoked very early
2334
 * in the page load, such as during hook_boot(), or unless you call
2335
 * menu_execute_active_handler() to generate your page output.
2336
 *
2337
 * @param $path
2338
 *   A Drupal path - not a path alias.
2339
 */
2340
function menu_set_active_item($path) {
2341
  $_GET['q'] = $path;
2342
  // Since the active item has changed, the active menu trail may also be out
2343
  // of date.
2344
  drupal_static_reset('menu_set_active_trail');
2345
}
2346

    
2347
/**
2348
 * Sets the active trail (path to the menu tree root) of the current page.
2349
 *
2350
 * Any trail set by this function will only be used for functionality that calls
2351
 * menu_get_active_trail(). Drupal core only uses trails set here for
2352
 * breadcrumbs and the page title and not for menu trees or page content.
2353
 * Additionally, breadcrumbs set by drupal_set_breadcrumb() will override any
2354
 * trail set here.
2355
 *
2356
 * To affect the trail used by menu trees, use menu_tree_set_path(). To affect
2357
 * the page content, use menu_set_active_item() instead.
2358
 *
2359
 * @param $new_trail
2360
 *   Menu trail to set; the value is saved in a static variable and can be
2361
 *   retrieved by menu_get_active_trail(). The format of this array should be
2362
 *   the same as the return value of menu_get_active_trail().
2363
 *
2364
 * @return
2365
 *   The active trail. See menu_get_active_trail() for details.
2366
 */
2367
function menu_set_active_trail($new_trail = NULL) {
2368
  $trail = &drupal_static(__FUNCTION__);
2369

    
2370
  if (isset($new_trail)) {
2371
    $trail = $new_trail;
2372
  }
2373
  elseif (!isset($trail)) {
2374
    $trail = array();
2375
    $trail[] = array(
2376
      'title' => t('Home'),
2377
      'href' => '<front>',
2378
      'link_path' => '',
2379
      'localized_options' => array(),
2380
      'type' => 0,
2381
    );
2382

    
2383
    // Try to retrieve a menu link corresponding to the current path. If more
2384
    // than one exists, the link from the most preferred menu is returned.
2385
    $preferred_link = menu_link_get_preferred();
2386
    $current_item = menu_get_item();
2387

    
2388
    // There is a link for the current path.
2389
    if ($preferred_link) {
2390
      // Pass TRUE for $only_active_trail to make menu_tree_page_data() build
2391
      // a stripped down menu tree containing the active trail only, in case
2392
      // the given menu has not been built in this request yet.
2393
      $tree = menu_tree_page_data($preferred_link['menu_name'], NULL, TRUE);
2394
      list($key, $curr) = each($tree);
2395
    }
2396
    // There is no link for the current path.
2397
    else {
2398
      $preferred_link = $current_item;
2399
      $curr = FALSE;
2400
    }
2401

    
2402
    while ($curr) {
2403
      $link = $curr['link'];
2404
      if ($link['in_active_trail']) {
2405
        // Add the link to the trail, unless it links to its parent.
2406
        if (!($link['type'] & MENU_LINKS_TO_PARENT)) {
2407
          // The menu tree for the active trail may contain additional links
2408
          // that have not been translated yet, since they contain dynamic
2409
          // argument placeholders (%). Such links are not contained in regular
2410
          // menu trees, and have only been loaded for the additional
2411
          // translation that happens here, so as to be able to display them in
2412
          // the breadcumb for the current page.
2413
          // @see _menu_tree_check_access()
2414
          // @see _menu_link_translate()
2415
          if (strpos($link['href'], '%') !== FALSE) {
2416
            _menu_link_translate($link, TRUE);
2417
          }
2418
          if ($link['access']) {
2419
            $trail[] = $link;
2420
          }
2421
        }
2422
        $tree = $curr['below'] ? $curr['below'] : array();
2423
      }
2424
      list($key, $curr) = each($tree);
2425
    }
2426
    // Make sure the current page is in the trail to build the page title, by
2427
    // appending either the preferred link or the menu router item for the
2428
    // current page. Exclude it if we are on the front page.
2429
    $last = end($trail);
2430
    if ($preferred_link && $last['href'] != $preferred_link['href'] && !drupal_is_front_page()) {
2431
      $trail[] = $preferred_link;
2432
    }
2433
  }
2434
  return $trail;
2435
}
2436

    
2437
/**
2438
 * Looks up the preferred menu link for a given system path.
2439
 *
2440
 * @param $path
2441
 *   The path; for example, 'node/5'. The function will find the corresponding
2442
 *   menu link ('node/5' if it exists, or fallback to 'node/%').
2443
 * @param $selected_menu
2444
 *   The name of a menu used to restrict the search for a preferred menu link.
2445
 *   If not specified, all the menus returned by menu_get_active_menu_names()
2446
 *   will be used.
2447
 *
2448
 * @return
2449
 *   A fully translated menu link, or FALSE if no matching menu link was
2450
 *   found. The most specific menu link ('node/5' preferred over 'node/%') in
2451
 *   the most preferred menu (as defined by menu_get_active_menu_names()) is
2452
 *   returned.
2453
 */
2454
function menu_link_get_preferred($path = NULL, $selected_menu = NULL) {
2455
  $preferred_links = &drupal_static(__FUNCTION__);
2456

    
2457
  if (!isset($path)) {
2458
    $path = $_GET['q'];
2459
  }
2460

    
2461
  if (empty($selected_menu)) {
2462
    // Use an illegal menu name as the key for the preferred menu link.
2463
    $selected_menu = MENU_PREFERRED_LINK;
2464
  }
2465

    
2466
  if (!isset($preferred_links[$path])) {
2467
    // Look for the correct menu link by building a list of candidate paths,
2468
    // which are ordered by priority (translated hrefs are preferred over
2469
    // untranslated paths). Afterwards, the most relevant path is picked from
2470
    // the menus, ordered by menu preference.
2471
    $item = menu_get_item($path);
2472
    $path_candidates = array();
2473
    // 1. The current item href.
2474
    $path_candidates[$item['href']] = $item['href'];
2475
    // 2. The tab root href of the current item (if any).
2476
    if ($item['tab_parent'] && ($tab_root = menu_get_item($item['tab_root_href']))) {
2477
      $path_candidates[$tab_root['href']] = $tab_root['href'];
2478
    }
2479
    // 3. The current item path (with wildcards).
2480
    $path_candidates[$item['path']] = $item['path'];
2481
    // 4. The tab root path of the current item (if any).
2482
    if (!empty($tab_root)) {
2483
      $path_candidates[$tab_root['path']] = $tab_root['path'];
2484
    }
2485

    
2486
    // Retrieve a list of menu names, ordered by preference.
2487
    $menu_names = menu_get_active_menu_names();
2488
    // Put the selected menu at the front of the list.
2489
    array_unshift($menu_names, $selected_menu);
2490

    
2491
    $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
2492
    $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
2493
    $query->fields('ml');
2494
    // Weight must be taken from {menu_links}, not {menu_router}.
2495
    $query->addField('ml', 'weight', 'link_weight');
2496
    $query->fields('m');
2497
    $query->condition('ml.link_path', $path_candidates, 'IN');
2498

    
2499
    // Sort candidates by link path and menu name.
2500
    $candidates = array();
2501
    foreach ($query->execute() as $candidate) {
2502
      $candidate['weight'] = $candidate['link_weight'];
2503
      $candidates[$candidate['link_path']][$candidate['menu_name']] = $candidate;
2504
      // Add any menus not already in the menu name search list.
2505
      if (!in_array($candidate['menu_name'], $menu_names)) {
2506
        $menu_names[] = $candidate['menu_name'];
2507
      }
2508
    }
2509

    
2510
    // Store the most specific link for each menu. Also save the most specific
2511
    // link of the most preferred menu in $preferred_link.
2512
    foreach ($path_candidates as $link_path) {
2513
      if (isset($candidates[$link_path])) {
2514
        foreach ($menu_names as $menu_name) {
2515
          if (empty($preferred_links[$path][$menu_name]) && isset($candidates[$link_path][$menu_name])) {
2516
            $candidate_item = $candidates[$link_path][$menu_name];
2517
            $map = explode('/', $path);
2518
            _menu_translate($candidate_item, $map);
2519
            if ($candidate_item['access']) {
2520
              $preferred_links[$path][$menu_name] = $candidate_item;
2521
              if (empty($preferred_links[$path][MENU_PREFERRED_LINK])) {
2522
                // Store the most specific link.
2523
                $preferred_links[$path][MENU_PREFERRED_LINK] = $candidate_item;
2524
              }
2525
            }
2526
          }
2527
        }
2528
      }
2529
    }
2530
  }
2531

    
2532
  return isset($preferred_links[$path][$selected_menu]) ? $preferred_links[$path][$selected_menu] : FALSE;
2533
}
2534

    
2535
/**
2536
 * Gets the active trail (path to root menu root) of the current page.
2537
 *
2538
 * If a trail is supplied to menu_set_active_trail(), that value is returned. If
2539
 * a trail is not supplied to menu_set_active_trail(), the path to the current
2540
 * page is calculated and returned. The calculated trail is also saved as a
2541
 * static value for use by subsequent calls to menu_get_active_trail().
2542
 *
2543
 * @return
2544
 *   Path to menu root of the current page, as an array of menu link items,
2545
 *   starting with the site's home page. Each link item is an associative array
2546
 *   with the following components:
2547
 *   - title: Title of the item.
2548
 *   - href: Drupal path of the item.
2549
 *   - localized_options: Options for passing into the l() function.
2550
 *   - type: A menu type constant, such as MENU_DEFAULT_LOCAL_TASK, or 0 to
2551
 *     indicate it's not really in the menu (used for the home page item).
2552
 */
2553
function menu_get_active_trail() {
2554
  return menu_set_active_trail();
2555
}
2556

    
2557
/**
2558
 * Gets the breadcrumb for the current page, as determined by the active trail.
2559
 *
2560
 * @see menu_set_active_trail()
2561
 */
2562
function menu_get_active_breadcrumb() {
2563
  $breadcrumb = array();
2564

    
2565
  // No breadcrumb for the front page.
2566
  if (drupal_is_front_page()) {
2567
    return $breadcrumb;
2568
  }
2569

    
2570
  $item = menu_get_item();
2571
  if (!empty($item['access'])) {
2572
    $active_trail = menu_get_active_trail();
2573

    
2574
    // Allow modules to alter the breadcrumb, if possible, as that is much
2575
    // faster than rebuilding an entirely new active trail.
2576
    drupal_alter('menu_breadcrumb', $active_trail, $item);
2577

    
2578
    // Don't show a link to the current page in the breadcrumb trail.
2579
    $end = end($active_trail);
2580
    if ($item['href'] == $end['href']) {
2581
      array_pop($active_trail);
2582
    }
2583

    
2584
    // Remove the tab root (parent) if the current path links to its parent.
2585
    // Normally, the tab root link is included in the breadcrumb, as soon as we
2586
    // are on a local task or any other child link. However, if we are on a
2587
    // default local task (e.g., node/%/view), then we do not want the tab root
2588
    // link (e.g., node/%) to appear, as it would be identical to the current
2589
    // page. Since this behavior also needs to work recursively (i.e., on
2590
    // default local tasks of default local tasks), and since the last non-task
2591
    // link in the trail is used as page title (see menu_get_active_title()),
2592
    // this condition cannot be cleanly integrated into menu_get_active_trail().
2593
    // menu_get_active_trail() already skips all links that link to their parent
2594
    // (commonly MENU_DEFAULT_LOCAL_TASK). In order to also hide the parent link
2595
    // itself, we always remove the last link in the trail, if the current
2596
    // router item links to its parent.
2597
    if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) {
2598
      array_pop($active_trail);
2599
    }
2600

    
2601
    foreach ($active_trail as $parent) {
2602
      $breadcrumb[] = l($parent['title'], $parent['href'], $parent['localized_options']);
2603
    }
2604
  }
2605
  return $breadcrumb;
2606
}
2607

    
2608
/**
2609
 * Gets the title of the current page, as determined by the active trail.
2610
 */
2611
function menu_get_active_title() {
2612
  $active_trail = menu_get_active_trail();
2613

    
2614
  foreach (array_reverse($active_trail) as $item) {
2615
    if (!(bool) ($item['type'] & MENU_IS_LOCAL_TASK)) {
2616
      return $item['title'];
2617
    }
2618
  }
2619
}
2620

    
2621
/**
2622
 * Gets a translated, access-checked menu link that is ready for rendering.
2623
 *
2624
 * This function should never be called from within node_load() or any other
2625
 * function used as a menu object load function since an infinite recursion may
2626
 * occur.
2627
 *
2628
 * @param $mlid
2629
 *   The mlid of the menu item.
2630
 *
2631
 * @return
2632
 *   A menu link, with $item['access'] filled and link translated for
2633
 *   rendering.
2634
 */
2635
function menu_link_load($mlid) {
2636
  if (is_numeric($mlid)) {
2637
    $query = db_select('menu_links', 'ml');
2638
    $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
2639
    $query->fields('ml');
2640
    // Weight should be taken from {menu_links}, not {menu_router}.
2641
    $query->addField('ml', 'weight', 'link_weight');
2642
    $query->fields('m');
2643
    $query->condition('ml.mlid', $mlid);
2644
    if ($item = $query->execute()->fetchAssoc()) {
2645
      $item['weight'] = $item['link_weight'];
2646
      _menu_link_translate($item);
2647
      return $item;
2648
    }
2649
  }
2650
  return FALSE;
2651
}
2652

    
2653
/**
2654
 * Clears the cached cached data for a single named menu.
2655
 */
2656
function menu_cache_clear($menu_name = 'navigation') {
2657
  $cache_cleared = &drupal_static(__FUNCTION__, array());
2658

    
2659
  if (empty($cache_cleared[$menu_name])) {
2660
    cache_clear_all('links:' . $menu_name . ':', 'cache_menu', TRUE);
2661
    $cache_cleared[$menu_name] = 1;
2662
  }
2663
  elseif ($cache_cleared[$menu_name] == 1) {
2664
    drupal_register_shutdown_function('cache_clear_all', 'links:' . $menu_name . ':', 'cache_menu', TRUE);
2665
    $cache_cleared[$menu_name] = 2;
2666
  }
2667

    
2668
  // Also clear the menu system static caches.
2669
  menu_reset_static_cache();
2670
}
2671

    
2672
/**
2673
 * Clears all cached menu data.
2674
 *
2675
 * This should be called any time broad changes
2676
 * might have been made to the router items or menu links.
2677
 */
2678
function menu_cache_clear_all() {
2679
  cache_clear_all('*', 'cache_menu', TRUE);
2680
  menu_reset_static_cache();
2681
}
2682

    
2683
/**
2684
 * Resets the menu system static cache.
2685
 */
2686
function menu_reset_static_cache() {
2687
  drupal_static_reset('_menu_build_tree');
2688
  drupal_static_reset('menu_tree');
2689
  drupal_static_reset('menu_tree_all_data');
2690
  drupal_static_reset('menu_tree_page_data');
2691
  drupal_static_reset('menu_load_all');
2692
  drupal_static_reset('menu_link_get_preferred');
2693
}
2694

    
2695
/**
2696
 * Populates the database tables used by various menu functions.
2697
 *
2698
 * This function will clear and populate the {menu_router} table, add entries
2699
 * to {menu_links} for new router items, and then remove stale items from
2700
 * {menu_links}. If called from update.php or install.php, it will also
2701
 * schedule a call to itself on the first real page load from
2702
 * menu_execute_active_handler(), because the maintenance page environment
2703
 * is different and leaves stale data in the menu tables.
2704
 *
2705
 * @return
2706
 *   TRUE if the menu was rebuilt, FALSE if another thread was rebuilding
2707
 *   in parallel and the current thread just waited for completion.
2708
 */
2709
function menu_rebuild() {
2710
  if (!lock_acquire('menu_rebuild')) {
2711
    // Wait for another request that is already doing this work.
2712
    // We choose to block here since otherwise the router item may not
2713
    // be available in menu_execute_active_handler() resulting in a 404.
2714
    lock_wait('menu_rebuild');
2715
    return FALSE;
2716
  }
2717

    
2718
  $transaction = db_transaction();
2719

    
2720
  try {
2721
    list($menu, $masks) = menu_router_build();
2722
    _menu_router_save($menu, $masks);
2723
    _menu_navigation_links_rebuild($menu);
2724
    // Clear the menu, page and block caches.
2725
    menu_cache_clear_all();
2726
    _menu_clear_page_cache();
2727

    
2728
    if (defined('MAINTENANCE_MODE')) {
2729
      variable_set('menu_rebuild_needed', TRUE);
2730
    }
2731
    else {
2732
      variable_del('menu_rebuild_needed');
2733
    }
2734
  }
2735
  catch (Exception $e) {
2736
    $transaction->rollback();
2737
    watchdog_exception('menu', $e);
2738
  }
2739

    
2740
  lock_release('menu_rebuild');
2741
  return TRUE;
2742
}
2743

    
2744
/**
2745
 * Collects and alters the menu definitions.
2746
 */
2747
function menu_router_build() {
2748
  // We need to manually call each module so that we can know which module
2749
  // a given item came from.
2750
  $callbacks = array();
2751
  foreach (module_implements('menu') as $module) {
2752
    $router_items = call_user_func($module . '_menu');
2753
    if (isset($router_items) && is_array($router_items)) {
2754
      foreach (array_keys($router_items) as $path) {
2755
        $router_items[$path]['module'] = $module;
2756
      }
2757
      $callbacks = array_merge($callbacks, $router_items);
2758
    }
2759
  }
2760
  // Alter the menu as defined in modules, keys are like user/%user.
2761
  drupal_alter('menu', $callbacks);
2762
  list($menu, $masks) = _menu_router_build($callbacks);
2763
  _menu_router_cache($menu);
2764

    
2765
  return array($menu, $masks);
2766
}
2767

    
2768
/**
2769
 * Stores the menu router if we have it in memory.
2770
 */
2771
function _menu_router_cache($new_menu = NULL) {
2772
  $menu = &drupal_static(__FUNCTION__);
2773

    
2774
  if (isset($new_menu)) {
2775
    $menu = $new_menu;
2776
  }
2777
  return $menu;
2778
}
2779

    
2780
/**
2781
 * Gets the menu router.
2782
 */
2783
function menu_get_router() {
2784
  // Check first if we have it in memory already.
2785
  $menu = _menu_router_cache();
2786
  if (empty($menu)) {
2787
    list($menu, $masks) = menu_router_build();
2788
  }
2789
  return $menu;
2790
}
2791

    
2792
/**
2793
 * Builds a link from a router item.
2794
 */
2795
function _menu_link_build($item) {
2796
  // Suggested items are disabled by default.
2797
  if ($item['type'] == MENU_SUGGESTED_ITEM) {
2798
    $item['hidden'] = 1;
2799
  }
2800
  // Hide all items that are not visible in the tree.
2801
  elseif (!($item['type'] & MENU_VISIBLE_IN_TREE)) {
2802
    $item['hidden'] = -1;
2803
  }
2804
  // Note, we set this as 'system', so that we can be sure to distinguish all
2805
  // the menu links generated automatically from entries in {menu_router}.
2806
  $item['module'] = 'system';
2807
  $item += array(
2808
    'menu_name' => 'navigation',
2809
    'link_title' => $item['title'],
2810
    'link_path' => $item['path'],
2811
    'hidden' => 0,
2812
    'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])),
2813
  );
2814
  return $item;
2815
}
2816

    
2817
/**
2818
 * Builds menu links for the items in the menu router.
2819
 */
2820
function _menu_navigation_links_rebuild($menu) {
2821
  // Add normal and suggested items as links.
2822
  $menu_links = array();
2823
  foreach ($menu as $path => $item) {
2824
    if ($item['_visible']) {
2825
      $menu_links[$path] = $item;
2826
      $sort[$path] = $item['_number_parts'];
2827
    }
2828
  }
2829
  if ($menu_links) {
2830
    // Keep an array of processed menu links, to allow menu_link_save() to
2831
    // check this for parents instead of querying the database.
2832
    $parent_candidates = array();
2833
    // Make sure no child comes before its parent.
2834
    array_multisort($sort, SORT_NUMERIC, $menu_links);
2835

    
2836
    foreach ($menu_links as $key => $item) {
2837
      $existing_item = db_select('menu_links')
2838
        ->fields('menu_links')
2839
        ->condition('link_path', $item['path'])
2840
        ->condition('module', 'system')
2841
        ->execute()->fetchAssoc();
2842
      if ($existing_item) {
2843
        $item['mlid'] = $existing_item['mlid'];
2844
        // A change in hook_menu may move the link to a different menu
2845
        if (empty($item['menu_name']) || ($item['menu_name'] == $existing_item['menu_name'])) {
2846
          $item['menu_name'] = $existing_item['menu_name'];
2847
          $item['plid'] = $existing_item['plid'];
2848
        }
2849
        else {
2850
          // It moved to a new menu. Let menu_link_save() try to find a new
2851
          // parent based on the path.
2852
          unset($item['plid']);
2853
        }
2854
        $item['has_children'] = $existing_item['has_children'];
2855
        $item['updated'] = $existing_item['updated'];
2856
      }
2857
      if ($existing_item && $existing_item['customized']) {
2858
        $parent_candidates[$existing_item['mlid']] = $existing_item;
2859
      }
2860
      else {
2861
        $item = _menu_link_build($item);
2862
        menu_link_save($item, $existing_item, $parent_candidates);
2863
        $parent_candidates[$item['mlid']] = $item;
2864
        unset($menu_links[$key]);
2865
      }
2866
    }
2867
  }
2868
  $paths = array_keys($menu);
2869
  // Updated and customized items whose router paths are gone need new ones.
2870
  $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC))
2871
    ->fields('menu_links', array(
2872
      'link_path',
2873
      'mlid',
2874
      'router_path',
2875
      'updated',
2876
    ))
2877
    ->condition(db_or()
2878
      ->condition('updated', 1)
2879
      ->condition(db_and()
2880
        ->condition('router_path', $paths, 'NOT IN')
2881
        ->condition('external', 0)
2882
        ->condition('customized', 1)
2883
      )
2884
    )
2885
    ->execute();
2886
  foreach ($result as $item) {
2887
    $router_path = _menu_find_router_path($item['link_path']);
2888
    if (!empty($router_path) && ($router_path != $item['router_path'] || $item['updated'])) {
2889
      // If the router path and the link path matches, it's surely a working
2890
      // item, so we clear the updated flag.
2891
      $updated = $item['updated'] && $router_path != $item['link_path'];
2892
      db_update('menu_links')
2893
        ->fields(array(
2894
          'router_path' => $router_path,
2895
          'updated' => (int) $updated,
2896
        ))
2897
        ->condition('mlid', $item['mlid'])
2898
        ->execute();
2899
    }
2900
  }
2901
  // Find any item whose router path does not exist any more.
2902
  $result = db_select('menu_links')
2903
    ->fields('menu_links')
2904
    ->condition('router_path', $paths, 'NOT IN')
2905
    ->condition('external', 0)
2906
    ->condition('updated', 0)
2907
    ->condition('customized', 0)
2908
    ->orderBy('depth', 'DESC')
2909
    ->execute();
2910
  // Remove all such items. Starting from those with the greatest depth will
2911
  // minimize the amount of re-parenting done by menu_link_delete().
2912
  foreach ($result as $item) {
2913
    _menu_delete_item($item, TRUE);
2914
  }
2915
}
2916

    
2917
/**
2918
 * Clones an array of menu links.
2919
 *
2920
 * @param $links
2921
 *   An array of menu links to clone.
2922
 * @param $menu_name
2923
 *   (optional) The name of a menu that the links will be cloned for. If not
2924
 *   set, the cloned links will be in the same menu as the original set of
2925
 *   links that were passed in.
2926
 *
2927
 * @return
2928
 *   An array of menu links with the same properties as the passed-in array,
2929
 *   but with the link identifiers removed so that a new link will be created
2930
 *   when any of them is passed in to menu_link_save().
2931
 *
2932
 * @see menu_link_save()
2933
 */
2934
function menu_links_clone($links, $menu_name = NULL) {
2935
  foreach ($links as &$link) {
2936
    unset($link['mlid']);
2937
    unset($link['plid']);
2938
    if (isset($menu_name)) {
2939
      $link['menu_name'] = $menu_name;
2940
    }
2941
  }
2942
  return $links;
2943
}
2944

    
2945
/**
2946
 * Returns an array containing all links for a menu.
2947
 *
2948
 * @param $menu_name
2949
 *   The name of the menu whose links should be returned.
2950
 *
2951
 * @return
2952
 *   An array of menu links.
2953
 */
2954
function menu_load_links($menu_name) {
2955
  $links = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC))
2956
    ->fields('ml')
2957
    ->condition('ml.menu_name', $menu_name)
2958
    // Order by weight so as to be helpful for menus that are only one level
2959
    // deep.
2960
    ->orderBy('weight')
2961
    ->execute()
2962
    ->fetchAll();
2963

    
2964
  foreach ($links as &$link) {
2965
    $link['options'] = unserialize($link['options']);
2966
  }
2967
  return $links;
2968
}
2969

    
2970
/**
2971
 * Deletes all links for a menu.
2972
 *
2973
 * @param $menu_name
2974
 *   The name of the menu whose links will be deleted.
2975
 */
2976
function menu_delete_links($menu_name) {
2977
  $links = menu_load_links($menu_name);
2978
  foreach ($links as $link) {
2979
    // To speed up the deletion process, we reset some link properties that
2980
    // would trigger re-parenting logic in _menu_delete_item() and
2981
    // _menu_update_parental_status().
2982
    $link['has_children'] = FALSE;
2983
    $link['plid'] = 0;
2984
    _menu_delete_item($link);
2985
  }
2986
}
2987

    
2988
/**
2989
 * Delete one or several menu links.
2990
 *
2991
 * @param $mlid
2992
 *   A valid menu link mlid or NULL. If NULL, $path is used.
2993
 * @param $path
2994
 *   The path to the menu items to be deleted. $mlid must be NULL.
2995
 */
2996
function menu_link_delete($mlid, $path = NULL) {
2997
  if (isset($mlid)) {
2998
    _menu_delete_item(db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc());
2999
  }
3000
  else {
3001
    $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(':link_path' => $path));
3002
    foreach ($result as $link) {
3003
      _menu_delete_item($link);
3004
    }
3005
  }
3006
}
3007

    
3008
/**
3009
 * Deletes a single menu link.
3010
 *
3011
 * @param $item
3012
 *   Item to be deleted.
3013
 * @param $force
3014
 *   Forces deletion. Internal use only, setting to TRUE is discouraged.
3015
 *
3016
 * @see menu_link_delete()
3017
 */
3018
function _menu_delete_item($item, $force = FALSE) {
3019
  $item = is_object($item) ? get_object_vars($item) : $item;
3020
  if ($item && ($item['module'] != 'system' || $item['updated'] || $force)) {
3021
    // Children get re-attached to the item's parent.
3022
    if ($item['has_children']) {
3023
      $result = db_query("SELECT mlid FROM {menu_links} WHERE plid = :plid", array(':plid' => $item['mlid']));
3024
      foreach ($result as $m) {
3025
        $child = menu_link_load($m->mlid);
3026
        $child['plid'] = $item['plid'];
3027
        menu_link_save($child);
3028
      }
3029
    }
3030

    
3031
    // Notify modules we are deleting the item.
3032
    module_invoke_all('menu_link_delete', $item);
3033

    
3034
    db_delete('menu_links')->condition('mlid', $item['mlid'])->execute();
3035

    
3036
    // Update the has_children status of the parent.
3037
    _menu_update_parental_status($item);
3038
    menu_cache_clear($item['menu_name']);
3039
    _menu_clear_page_cache();
3040
  }
3041
}
3042

    
3043
/**
3044
 * Saves a menu link.
3045
 *
3046
 * After calling this function, rebuild the menu cache using
3047
 * menu_cache_clear_all().
3048
 *
3049
 * @param $item
3050
 *   An associative array representing a menu link item, with elements:
3051
 *   - link_path: (required) The path of the menu item, which should be
3052
 *     normalized first by calling drupal_get_normal_path() on it.
3053
 *   - link_title: (required) Title to appear in menu for the link.
3054
 *   - menu_name: (optional) The machine name of the menu for the link.
3055
 *     Defaults to 'navigation'.
3056
 *   - weight: (optional) Integer to determine position in menu. Default is 0.
3057
 *   - expanded: (optional) Boolean that determines if the item is expanded.
3058
 *   - options: (optional) An array of options, see l() for more.
3059
 *   - mlid: (optional) Menu link identifier, the primary integer key for each
3060
 *     menu link. Can be set to an existing value, or to 0 or NULL
3061
 *     to insert a new link.
3062
 *   - plid: (optional) The mlid of the parent.
3063
 *   - router_path: (optional) The path of the relevant router item.
3064
 * @param $existing_item
3065
 *   Optional, the current record from the {menu_links} table as an array.
3066
 * @param $parent_candidates
3067
 *   Optional array of menu links keyed by mlid. Used by
3068
 *   _menu_navigation_links_rebuild() only.
3069
 *
3070
 * @return
3071
 *   The mlid of the saved menu link, or FALSE if the menu link could not be
3072
 *   saved.
3073
 */
3074
function menu_link_save(&$item, $existing_item = array(), $parent_candidates = array()) {
3075
  drupal_alter('menu_link', $item);
3076

    
3077
  // This is the easiest way to handle the unique internal path '<front>',
3078
  // since a path marked as external does not need to match a router path.
3079
  $item['external'] = (url_is_external($item['link_path'])  || $item['link_path'] == '<front>') ? 1 : 0;
3080
  // Load defaults.
3081
  $item += array(
3082
    'menu_name' => 'navigation',
3083
    'weight' => 0,
3084
    'link_title' => '',
3085
    'hidden' => 0,
3086
    'has_children' => 0,
3087
    'expanded' => 0,
3088
    'options' => array(),
3089
    'module' => 'menu',
3090
    'customized' => 0,
3091
    'updated' => 0,
3092
  );
3093
  if (isset($item['mlid'])) {
3094
    if (!$existing_item) {
3095
      $existing_item = db_query('SELECT * FROM {menu_links} WHERE mlid = :mlid', array('mlid' => $item['mlid']))->fetchAssoc();
3096
    }
3097
    if ($existing_item) {
3098
      $existing_item['options'] = unserialize($existing_item['options']);
3099
    }
3100
  }
3101
  else {
3102
    $existing_item = FALSE;
3103
  }
3104

    
3105
  // Try to find a parent link. If found, assign it and derive its menu.
3106
  $parent = _menu_link_find_parent($item, $parent_candidates);
3107
  if (!empty($parent['mlid'])) {
3108
    $item['plid'] = $parent['mlid'];
3109
    $item['menu_name'] = $parent['menu_name'];
3110
  }
3111
  // If no corresponding parent link was found, move the link to the top-level.
3112
  else {
3113
    $item['plid'] = 0;
3114
  }
3115
  $menu_name = $item['menu_name'];
3116

    
3117
  if (!$existing_item) {
3118
    $item['mlid'] = db_insert('menu_links')
3119
      ->fields(array(
3120
        'menu_name' => $item['menu_name'],
3121
        'plid' => $item['plid'],
3122
        'link_path' => $item['link_path'],
3123
        'hidden' => $item['hidden'],
3124
        'external' => $item['external'],
3125
        'has_children' => $item['has_children'],
3126
        'expanded' => $item['expanded'],
3127
        'weight' => $item['weight'],
3128
        'module' => $item['module'],
3129
        'link_title' => $item['link_title'],
3130
        'options' => serialize($item['options']),
3131
        'customized' => $item['customized'],
3132
        'updated' => $item['updated'],
3133
      ))
3134
      ->execute();
3135
  }
3136

    
3137
  // Directly fill parents for top-level links.
3138
  if ($item['plid'] == 0) {
3139
    $item['p1'] = $item['mlid'];
3140
    for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
3141
      $item["p$i"] = 0;
3142
    }
3143
    $item['depth'] = 1;
3144
  }
3145
  // Otherwise, ensure that this link's depth is not beyond the maximum depth
3146
  // and fill parents based on the parent link.
3147
  else {
3148
    if ($item['has_children'] && $existing_item) {
3149
      $limit = MENU_MAX_DEPTH - menu_link_children_relative_depth($existing_item) - 1;
3150
    }
3151
    else {
3152
      $limit = MENU_MAX_DEPTH - 1;
3153
    }
3154
    if ($parent['depth'] > $limit) {
3155
      return FALSE;
3156
    }
3157
    $item['depth'] = $parent['depth'] + 1;
3158
    _menu_link_parents_set($item, $parent);
3159
  }
3160
  // Need to check both plid and menu_name, since plid can be 0 in any menu.
3161
  if ($existing_item && ($item['plid'] != $existing_item['plid'] || $menu_name != $existing_item['menu_name'])) {
3162
    _menu_link_move_children($item, $existing_item);
3163
  }
3164
  // Find the router_path.
3165
  if (empty($item['router_path'])  || !$existing_item || ($existing_item['link_path'] != $item['link_path'])) {
3166
    if ($item['external']) {
3167
      $item['router_path'] = '';
3168
    }
3169
    else {
3170
      // Find the router path which will serve this path.
3171
      $item['parts'] = explode('/', $item['link_path'], MENU_MAX_PARTS);
3172
      $item['router_path'] = _menu_find_router_path($item['link_path']);
3173
    }
3174
  }
3175
  // If every value in $existing_item is the same in the $item, there is no
3176
  // reason to run the update queries or clear the caches. We use
3177
  // array_intersect_key() with the $item as the first parameter because
3178
  // $item may have additional keys left over from building a router entry.
3179
  // The intersect removes the extra keys, allowing a meaningful comparison.
3180
  if (!$existing_item || (array_intersect_key($item, $existing_item) != $existing_item)) {
3181
    db_update('menu_links')
3182
      ->fields(array(
3183
        'menu_name' => $item['menu_name'],
3184
        'plid' => $item['plid'],
3185
        'link_path' => $item['link_path'],
3186
        'router_path' => $item['router_path'],
3187
        'hidden' => $item['hidden'],
3188
        'external' => $item['external'],
3189
        'has_children' => $item['has_children'],
3190
        'expanded' => $item['expanded'],
3191
        'weight' => $item['weight'],
3192
        'depth' => $item['depth'],
3193
        'p1' => $item['p1'],
3194
        'p2' => $item['p2'],
3195
        'p3' => $item['p3'],
3196
        'p4' => $item['p4'],
3197
        'p5' => $item['p5'],
3198
        'p6' => $item['p6'],
3199
        'p7' => $item['p7'],
3200
        'p8' => $item['p8'],
3201
        'p9' => $item['p9'],
3202
        'module' => $item['module'],
3203
        'link_title' => $item['link_title'],
3204
        'options' => serialize($item['options']),
3205
        'customized' => $item['customized'],
3206
      ))
3207
      ->condition('mlid', $item['mlid'])
3208
      ->execute();
3209
    // Check the has_children status of the parent.
3210
    _menu_update_parental_status($item);
3211
    menu_cache_clear($menu_name);
3212
    if ($existing_item && $menu_name != $existing_item['menu_name']) {
3213
      menu_cache_clear($existing_item['menu_name']);
3214
    }
3215
    // Notify modules we have acted on a menu item.
3216
    $hook = 'menu_link_insert';
3217
    if ($existing_item) {
3218
      $hook = 'menu_link_update';
3219
    }
3220
    module_invoke_all($hook, $item);
3221
    // Now clear the cache.
3222
    _menu_clear_page_cache();
3223
  }
3224
  return $item['mlid'];
3225
}
3226

    
3227
/**
3228
 * Finds a possible parent for a given menu link.
3229
 *
3230
 * Because the parent of a given link might not exist anymore in the database,
3231
 * we apply a set of heuristics to determine a proper parent:
3232
 *
3233
 *  - use the passed parent link if specified and existing.
3234
 *  - else, use the first existing link down the previous link hierarchy
3235
 *  - else, for system menu links (derived from hook_menu()), reparent
3236
 *    based on the path hierarchy.
3237
 *
3238
 * @param $menu_link
3239
 *   A menu link.
3240
 * @param $parent_candidates
3241
 *   An array of menu links keyed by mlid.
3242
 *
3243
 * @return
3244
 *   A menu link structure of the possible parent or FALSE if no valid parent
3245
 *   has been found.
3246
 */
3247
function _menu_link_find_parent($menu_link, $parent_candidates = array()) {
3248
  $parent = FALSE;
3249

    
3250
  // This item is explicitely top-level, skip the rest of the parenting.
3251
  if (isset($menu_link['plid']) && empty($menu_link['plid'])) {
3252
    return $parent;
3253
  }
3254

    
3255
  // If we have a parent link ID, try to use that.
3256
  $candidates = array();
3257
  if (isset($menu_link['plid'])) {
3258
    $candidates[] = $menu_link['plid'];
3259
  }
3260

    
3261
  // Else, if we have a link hierarchy try to find a valid parent in there.
3262
  if (!empty($menu_link['depth']) && $menu_link['depth'] > 1) {
3263
    for ($depth = $menu_link['depth'] - 1; $depth >= 1; $depth--) {
3264
      $candidates[] = $menu_link['p' . $depth];
3265
    }
3266
  }
3267

    
3268
  foreach ($candidates as $mlid) {
3269
    if (isset($parent_candidates[$mlid])) {
3270
      $parent = $parent_candidates[$mlid];
3271
    }
3272
    else {
3273
      $parent = db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc();
3274
    }
3275
    if ($parent) {
3276
      return $parent;
3277
    }
3278
  }
3279

    
3280
  // If everything else failed, try to derive the parent from the path
3281
  // hierarchy. This only makes sense for links derived from menu router
3282
  // items (ie. from hook_menu()).
3283
  if ($menu_link['module'] == 'system') {
3284
    $query = db_select('menu_links');
3285
    $query->condition('module', 'system');
3286
    // We always respect the link's 'menu_name'; inheritance for router items is
3287
    // ensured in _menu_router_build().
3288
    $query->condition('menu_name', $menu_link['menu_name']);
3289

    
3290
    // Find the parent - it must be unique.
3291
    $parent_path = $menu_link['link_path'];
3292
    do {
3293
      $parent = FALSE;
3294
      $parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
3295
      $new_query = clone $query;
3296
      $new_query->condition('link_path', $parent_path);
3297
      // Only valid if we get a unique result.
3298
      if ($new_query->countQuery()->execute()->fetchField() == 1) {
3299
        $parent = $new_query->fields('menu_links')->execute()->fetchAssoc();
3300
      }
3301
    } while ($parent === FALSE && $parent_path);
3302
  }
3303

    
3304
  return $parent;
3305
}
3306

    
3307
/**
3308
 * Clears the page and block caches at most twice per page load.
3309
 */
3310
function _menu_clear_page_cache() {
3311
  $cache_cleared = &drupal_static(__FUNCTION__, 0);
3312

    
3313
  // Clear the page and block caches, but at most twice, including at
3314
  //  the end of the page load when there are multiple links saved or deleted.
3315
  if ($cache_cleared == 0) {
3316
    cache_clear_all();
3317
    // Keep track of which menus have expanded items.
3318
    _menu_set_expanded_menus();
3319
    $cache_cleared = 1;
3320
  }
3321
  elseif ($cache_cleared == 1) {
3322
    drupal_register_shutdown_function('cache_clear_all');
3323
    // Keep track of which menus have expanded items.
3324
    drupal_register_shutdown_function('_menu_set_expanded_menus');
3325
    $cache_cleared = 2;
3326
  }
3327
}
3328

    
3329
/**
3330
 * Updates a list of menus with expanded items.
3331
 */
3332
function _menu_set_expanded_menus() {
3333
  $names = db_query("SELECT menu_name FROM {menu_links} WHERE expanded <> 0 GROUP BY menu_name")->fetchCol();
3334
  variable_set('menu_expanded', $names);
3335
}
3336

    
3337
/**
3338
 * Finds the router path which will serve this path.
3339
 *
3340
 * @param $link_path
3341
 *  The path for we are looking up its router path.
3342
 *
3343
 * @return
3344
 *  A path from $menu keys or empty if $link_path points to a nonexisting
3345
 *  place.
3346
 */
3347
function _menu_find_router_path($link_path) {
3348
  // $menu will only have data during a menu rebuild.
3349
  $menu = _menu_router_cache();
3350

    
3351
  $router_path = $link_path;
3352
  $parts = explode('/', $link_path, MENU_MAX_PARTS);
3353
  $ancestors = menu_get_ancestors($parts);
3354

    
3355
  if (empty($menu)) {
3356
    // Not during a menu rebuild, so look up in the database.
3357
    $router_path = (string) db_select('menu_router')
3358
      ->fields('menu_router', array('path'))
3359
      ->condition('path', $ancestors, 'IN')
3360
      ->orderBy('fit', 'DESC')
3361
      ->range(0, 1)
3362
      ->execute()->fetchField();
3363
  }
3364
  elseif (!isset($menu[$router_path])) {
3365
    // Add an empty router path as a fallback.
3366
    $ancestors[] = '';
3367
    foreach ($ancestors as $key => $router_path) {
3368
      if (isset($menu[$router_path])) {
3369
        // Exit the loop leaving $router_path as the first match.
3370
        break;
3371
      }
3372
    }
3373
    // If we did not find the path, $router_path will be the empty string
3374
    // at the end of $ancestors.
3375
  }
3376
  return $router_path;
3377
}
3378

    
3379
/**
3380
 * Inserts, updates, or deletes an uncustomized menu link related to a module.
3381
 *
3382
 * @param $module
3383
 *   The name of the module.
3384
 * @param $op
3385
 *   Operation to perform: insert, update or delete.
3386
 * @param $link_path
3387
 *   The path this link points to.
3388
 * @param $link_title
3389
 *   Title of the link to insert or new title to update the link to.
3390
 *   Unused for delete.
3391
 *
3392
 * @return
3393
 *   The insert op returns the mlid of the new item. Others op return NULL.
3394
 */
3395
function menu_link_maintain($module, $op, $link_path, $link_title) {
3396
  switch ($op) {
3397
    case 'insert':
3398
      $menu_link = array(
3399
        'link_title' => $link_title,
3400
        'link_path' => $link_path,
3401
        'module' => $module,
3402
      );
3403
      return menu_link_save($menu_link);
3404
      break;
3405
    case 'update':
3406
      $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path AND module = :module AND customized = 0", array(':link_path' => $link_path, ':module' => $module))->fetchAll(PDO::FETCH_ASSOC);
3407
      foreach ($result as $link) {
3408
        $link['link_title'] = $link_title;
3409
        $link['options'] = unserialize($link['options']);
3410
        menu_link_save($link);
3411
      }
3412
      break;
3413
    case 'delete':
3414
      menu_link_delete(NULL, $link_path);
3415
      break;
3416
  }
3417
}
3418

    
3419
/**
3420
 * Finds the depth of an item's children relative to its depth.
3421
 *
3422
 * For example, if the item has a depth of 2, and the maximum of any child in
3423
 * the menu link tree is 5, the relative depth is 3.
3424
 *
3425
 * @param $item
3426
 *   An array representing a menu link item.
3427
 *
3428
 * @return
3429
 *   The relative depth, or zero.
3430
 *
3431
 */
3432
function menu_link_children_relative_depth($item) {
3433
  $query = db_select('menu_links');
3434
  $query->addField('menu_links', 'depth');
3435
  $query->condition('menu_name', $item['menu_name']);
3436
  $query->orderBy('depth', 'DESC');
3437
  $query->range(0, 1);
3438

    
3439
  $i = 1;
3440
  $p = 'p1';
3441
  while ($i <= MENU_MAX_DEPTH && $item[$p]) {
3442
    $query->condition($p, $item[$p]);
3443
    $p = 'p' . ++$i;
3444
  }
3445

    
3446
  $max_depth = $query->execute()->fetchField();
3447

    
3448
  return ($max_depth > $item['depth']) ? $max_depth - $item['depth'] : 0;
3449
}
3450

    
3451
/**
3452
 * Updates the children of a menu link that is being moved.
3453
 *
3454
 * The menu name, parents (p1 - p6), and depth are updated for all children of
3455
 * the link, and the has_children status of the previous parent is updated.
3456
 */
3457
function _menu_link_move_children($item, $existing_item) {
3458
  $query = db_update('menu_links');
3459

    
3460
  $query->fields(array('menu_name' => $item['menu_name']));
3461

    
3462
  $p = 'p1';
3463
  $expressions = array();
3464
  for ($i = 1; $i <= $item['depth']; $p = 'p' . ++$i) {
3465
    $expressions[] = array($p, ":p_$i", array(":p_$i" => $item[$p]));
3466
  }
3467
  $j = $existing_item['depth'] + 1;
3468
  while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) {
3469
    $expressions[] = array('p' . $i++, 'p' . $j++, array());
3470
  }
3471
  while ($i <= MENU_MAX_DEPTH) {
3472
    $expressions[] = array('p' . $i++, 0, array());
3473
  }
3474

    
3475
  $shift = $item['depth'] - $existing_item['depth'];
3476
  if ($shift > 0) {
3477
    // The order of expressions must be reversed so the new values don't
3478
    // overwrite the old ones before they can be used because "Single-table
3479
    // UPDATE assignments are generally evaluated from left to right"
3480
    // see: http://dev.mysql.com/doc/refman/5.0/en/update.html
3481
    $expressions = array_reverse($expressions);
3482
  }
3483
  foreach ($expressions as $expression) {
3484
    $query->expression($expression[0], $expression[1], $expression[2]);
3485
  }
3486

    
3487
  $query->expression('depth', 'depth + :depth', array(':depth' => $shift));
3488
  $query->condition('menu_name', $existing_item['menu_name']);
3489
  $p = 'p1';
3490
  for ($i = 1; $i <= MENU_MAX_DEPTH && $existing_item[$p]; $p = 'p' . ++$i) {
3491
    $query->condition($p, $existing_item[$p]);
3492
  }
3493

    
3494
  $query->execute();
3495

    
3496
  // Check the has_children status of the parent, while excluding this item.
3497
  _menu_update_parental_status($existing_item, TRUE);
3498
}
3499

    
3500
/**
3501
 * Checks and updates the 'has_children' status for the parent of a link.
3502
 */
3503
function _menu_update_parental_status($item, $exclude = FALSE) {
3504
  // If plid == 0, there is nothing to update.
3505
  if ($item['plid']) {
3506
    // Check if at least one visible child exists in the table.
3507
    $query = db_select('menu_links');
3508
    $query->addField('menu_links', 'mlid');
3509
    $query->condition('menu_name', $item['menu_name']);
3510
    $query->condition('hidden', 0);
3511
    $query->condition('plid', $item['plid']);
3512
    $query->range(0, 1);
3513
    if ($exclude) {
3514
      $query->condition('mlid', $item['mlid'], '<>');
3515
    }
3516
    $parent_has_children = ((bool) $query->execute()->fetchField()) ? 1 : 0;
3517
    db_update('menu_links')
3518
      ->fields(array('has_children' => $parent_has_children))
3519
      ->condition('mlid', $item['plid'])
3520
      ->execute();
3521
  }
3522
}
3523

    
3524
/**
3525
 * Sets the p1 through p9 values for a menu link being saved.
3526
 */
3527
function _menu_link_parents_set(&$item, $parent) {
3528
  $i = 1;
3529
  while ($i < $item['depth']) {
3530
    $p = 'p' . $i++;
3531
    $item[$p] = $parent[$p];
3532
  }
3533
  $p = 'p' . $i++;
3534
  // The parent (p1 - p9) corresponding to the depth always equals the mlid.
3535
  $item[$p] = $item['mlid'];
3536
  while ($i <= MENU_MAX_DEPTH) {
3537
    $p = 'p' . $i++;
3538
    $item[$p] = 0;
3539
  }
3540
}
3541

    
3542
/**
3543
 * Builds the router table based on the data from hook_menu().
3544
 */
3545
function _menu_router_build($callbacks) {
3546
  // First pass: separate callbacks from paths, making paths ready for
3547
  // matching. Calculate fitness, and fill some default values.
3548
  $menu = array();
3549
  $masks = array();
3550
  foreach ($callbacks as $path => $item) {
3551
    $load_functions = array();
3552
    $to_arg_functions = array();
3553
    $fit = 0;
3554
    $move = FALSE;
3555

    
3556
    $parts = explode('/', $path, MENU_MAX_PARTS);
3557
    $number_parts = count($parts);
3558
    // We store the highest index of parts here to save some work in the fit
3559
    // calculation loop.
3560
    $slashes = $number_parts - 1;
3561
    // Extract load and to_arg functions.
3562
    foreach ($parts as $k => $part) {
3563
      $match = FALSE;
3564
      // Look for wildcards in the form allowed to be used in PHP functions,
3565
      // because we are using these to construct the load function names.
3566
      if (preg_match('/^%(|' . DRUPAL_PHP_FUNCTION_PATTERN . ')$/', $part, $matches)) {
3567
        if (empty($matches[1])) {
3568
          $match = TRUE;
3569
          $load_functions[$k] = NULL;
3570
        }
3571
        else {
3572
          if (function_exists($matches[1] . '_to_arg')) {
3573
            $to_arg_functions[$k] = $matches[1] . '_to_arg';
3574
            $load_functions[$k] = NULL;
3575
            $match = TRUE;
3576
          }
3577
          if (function_exists($matches[1] . '_load')) {
3578
            $function = $matches[1] . '_load';
3579
            // Create an array of arguments that will be passed to the _load
3580
            // function when this menu path is checked, if 'load arguments'
3581
            // exists.
3582
            $load_functions[$k] = isset($item['load arguments']) ? array($function => $item['load arguments']) : $function;
3583
            $match = TRUE;
3584
          }
3585
        }
3586
      }
3587
      if ($match) {
3588
        $parts[$k] = '%';
3589
      }
3590
      else {
3591
        $fit |=  1 << ($slashes - $k);
3592
      }
3593
    }
3594
    if ($fit) {
3595
      $move = TRUE;
3596
    }
3597
    else {
3598
      // If there is no %, it fits maximally.
3599
      $fit = (1 << $number_parts) - 1;
3600
    }
3601
    $masks[$fit] = 1;
3602
    $item['_load_functions'] = $load_functions;
3603
    $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions);
3604
    $item += array(
3605
      'title' => '',
3606
      'weight' => 0,
3607
      'type' => MENU_NORMAL_ITEM,
3608
      'module' => '',
3609
      '_number_parts' => $number_parts,
3610
      '_parts' => $parts,
3611
      '_fit' => $fit,
3612
    );
3613
    $item += array(
3614
      '_visible' => (bool) ($item['type'] & MENU_VISIBLE_IN_BREADCRUMB),
3615
      '_tab' => (bool) ($item['type'] & MENU_IS_LOCAL_TASK),
3616
    );
3617
    if ($move) {
3618
      $new_path = implode('/', $item['_parts']);
3619
      $menu[$new_path] = $item;
3620
      $sort[$new_path] = $number_parts;
3621
    }
3622
    else {
3623
      $menu[$path] = $item;
3624
      $sort[$path] = $number_parts;
3625
    }
3626
  }
3627
  array_multisort($sort, SORT_NUMERIC, $menu);
3628
  // Apply inheritance rules.
3629
  foreach ($menu as $path => $v) {
3630
    $item = &$menu[$path];
3631
    if (!$item['_tab']) {
3632
      // Non-tab items.
3633
      $item['tab_parent'] = '';
3634
      $item['tab_root'] = $path;
3635
    }
3636
    // If not specified, assign the default tab type for local tasks.
3637
    elseif (!isset($item['context'])) {
3638
      $item['context'] = MENU_CONTEXT_PAGE;
3639
    }
3640
    for ($i = $item['_number_parts'] - 1; $i; $i--) {
3641
      $parent_path = implode('/', array_slice($item['_parts'], 0, $i));
3642
      if (isset($menu[$parent_path])) {
3643

    
3644
        $parent = &$menu[$parent_path];
3645

    
3646
        // If we have no menu name, try to inherit it from parent items.
3647
        if (!isset($item['menu_name'])) {
3648
          // If the parent item of this item does not define a menu name (and no
3649
          // previous iteration assigned one already), try to find the menu name
3650
          // of the parent item in the currently stored menu links.
3651
          if (!isset($parent['menu_name'])) {
3652
            $menu_name = db_query("SELECT menu_name FROM {menu_links} WHERE router_path = :router_path AND module = 'system'", array(':router_path' => $parent_path))->fetchField();
3653
            if ($menu_name) {
3654
              $parent['menu_name'] = $menu_name;
3655
            }
3656
          }
3657
          // If the parent item defines a menu name, inherit it.
3658
          if (!empty($parent['menu_name'])) {
3659
            $item['menu_name'] = $parent['menu_name'];
3660
          }
3661
        }
3662
        if (!isset($item['tab_parent'])) {
3663
          // Parent stores the parent of the path.
3664
          $item['tab_parent'] = $parent_path;
3665
        }
3666
        if (!isset($item['tab_root']) && !$parent['_tab']) {
3667
          $item['tab_root'] = $parent_path;
3668
        }
3669
        // If an access callback is not found for a default local task we use
3670
        // the callback from the parent, since we expect them to be identical.
3671
        // In all other cases, the access parameters must be specified.
3672
        if (($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access callback']) && isset($parent['access callback'])) {
3673
          $item['access callback'] = $parent['access callback'];
3674
          if (!isset($item['access arguments']) && isset($parent['access arguments'])) {
3675
            $item['access arguments'] = $parent['access arguments'];
3676
          }
3677
        }
3678
        // Same for page callbacks.
3679
        if (!isset($item['page callback']) && isset($parent['page callback'])) {
3680
          $item['page callback'] = $parent['page callback'];
3681
          if (!isset($item['page arguments']) && isset($parent['page arguments'])) {
3682
            $item['page arguments'] = $parent['page arguments'];
3683
          }
3684
          if (!isset($item['file path']) && isset($parent['file path'])) {
3685
            $item['file path'] = $parent['file path'];
3686
          }
3687
          if (!isset($item['file']) && isset($parent['file'])) {
3688
            $item['file'] = $parent['file'];
3689
            if (empty($item['file path']) && isset($item['module']) && isset($parent['module']) && $item['module'] != $parent['module']) {
3690
              $item['file path'] = drupal_get_path('module', $parent['module']);
3691
            }
3692
          }
3693
        }
3694
        // Same for delivery callbacks.
3695
        if (!isset($item['delivery callback']) && isset($parent['delivery callback'])) {
3696
          $item['delivery callback'] = $parent['delivery callback'];
3697
        }
3698
        // Same for theme callbacks.
3699
        if (!isset($item['theme callback']) && isset($parent['theme callback'])) {
3700
          $item['theme callback'] = $parent['theme callback'];
3701
          if (!isset($item['theme arguments']) && isset($parent['theme arguments'])) {
3702
            $item['theme arguments'] = $parent['theme arguments'];
3703
          }
3704
        }
3705
        // Same for load arguments: if a loader doesn't have any explict
3706
        // arguments, try to find arguments in the parent.
3707
        if (!isset($item['load arguments'])) {
3708
          foreach ($item['_load_functions'] as $k => $function) {
3709
            // This loader doesn't have any explict arguments...
3710
            if (!is_array($function)) {
3711
              // ... check the parent for a loader at the same position
3712
              // using the same function name and defining arguments...
3713
              if (isset($parent['_load_functions'][$k]) && is_array($parent['_load_functions'][$k]) && key($parent['_load_functions'][$k]) === $function) {
3714
                // ... and inherit the arguments on the child.
3715
                $item['_load_functions'][$k] = $parent['_load_functions'][$k];
3716
              }
3717
            }
3718
          }
3719
        }
3720
      }
3721
    }
3722
    if (!isset($item['access callback']) && isset($item['access arguments'])) {
3723
      // Default callback.
3724
      $item['access callback'] = 'user_access';
3725
    }
3726
    if (!isset($item['access callback']) || empty($item['page callback'])) {
3727
      $item['access callback'] = 0;
3728
    }
3729
    if (is_bool($item['access callback'])) {
3730
      $item['access callback'] = intval($item['access callback']);
3731
    }
3732

    
3733
    $item['load_functions'] = empty($item['_load_functions']) ? '' : serialize($item['_load_functions']);
3734
    $item += array(
3735
      'access arguments' => array(),
3736
      'access callback' => '',
3737
      'page arguments' => array(),
3738
      'page callback' => '',
3739
      'delivery callback' => '',
3740
      'title arguments' => array(),
3741
      'title callback' => 't',
3742
      'theme arguments' => array(),
3743
      'theme callback' => '',
3744
      'description' => '',
3745
      'position' => '',
3746
      'context' => 0,
3747
      'tab_parent' => '',
3748
      'tab_root' => $path,
3749
      'path' => $path,
3750
      'file' => '',
3751
      'file path' => '',
3752
      'include file' => '',
3753
    );
3754

    
3755
    // Calculate out the file to be included for each callback, if any.
3756
    if ($item['file']) {
3757
      $file_path = $item['file path'] ? $item['file path'] : drupal_get_path('module', $item['module']);
3758
      $item['include file'] = $file_path . '/' . $item['file'];
3759
    }
3760
  }
3761

    
3762
  // Sort the masks so they are in order of descending fit.
3763
  $masks = array_keys($masks);
3764
  rsort($masks);
3765

    
3766
  return array($menu, $masks);
3767
}
3768

    
3769
/**
3770
 * Saves data from menu_router_build() to the router table.
3771
 */
3772
function _menu_router_save($menu, $masks) {
3773
  // Delete the existing router since we have some data to replace it.
3774
  db_truncate('menu_router')->execute();
3775

    
3776
  // Prepare insert object.
3777
  $insert = db_insert('menu_router')
3778
    ->fields(array(
3779
      'path',
3780
      'load_functions',
3781
      'to_arg_functions',
3782
      'access_callback',
3783
      'access_arguments',
3784
      'page_callback',
3785
      'page_arguments',
3786
      'delivery_callback',
3787
      'fit',
3788
      'number_parts',
3789
      'context',
3790
      'tab_parent',
3791
      'tab_root',
3792
      'title',
3793
      'title_callback',
3794
      'title_arguments',
3795
      'theme_callback',
3796
      'theme_arguments',
3797
      'type',
3798
      'description',
3799
      'position',
3800
      'weight',
3801
      'include_file',
3802
    ));
3803

    
3804
  $num_records = 0;
3805

    
3806
  foreach ($menu as $path => $item) {
3807
    // Fill in insert object values.
3808
    $insert->values(array(
3809
      'path' => $item['path'],
3810
      'load_functions' => $item['load_functions'],
3811
      'to_arg_functions' => $item['to_arg_functions'],
3812
      'access_callback' => $item['access callback'],
3813
      'access_arguments' => serialize($item['access arguments']),
3814
      'page_callback' => $item['page callback'],
3815
      'page_arguments' => serialize($item['page arguments']),
3816
      'delivery_callback' => $item['delivery callback'],
3817
      'fit' => $item['_fit'],
3818
      'number_parts' => $item['_number_parts'],
3819
      'context' => $item['context'],
3820
      'tab_parent' => $item['tab_parent'],
3821
      'tab_root' => $item['tab_root'],
3822
      'title' => $item['title'],
3823
      'title_callback' => $item['title callback'],
3824
      'title_arguments' => ($item['title arguments'] ? serialize($item['title arguments']) : ''),
3825
      'theme_callback' => $item['theme callback'],
3826
      'theme_arguments' => serialize($item['theme arguments']),
3827
      'type' => $item['type'],
3828
      'description' => $item['description'],
3829
      'position' => $item['position'],
3830
      'weight' => $item['weight'],
3831
      'include_file' => $item['include file'],
3832
    ));
3833

    
3834
    // Execute in batches to avoid the memory overhead of all of those records
3835
    // in the query object.
3836
    if (++$num_records == 20) {
3837
      $insert->execute();
3838
      $num_records = 0;
3839
    }
3840
  }
3841
  // Insert any remaining records.
3842
  $insert->execute();
3843
  // Store the masks.
3844
  variable_set('menu_masks', $masks);
3845

    
3846
  return $menu;
3847
}
3848

    
3849
/**
3850
 * Checks whether the site is in maintenance mode.
3851
 *
3852
 * This function will log the current user out and redirect to front page
3853
 * if the current user has no 'access site in maintenance mode' permission.
3854
 *
3855
 * @param $check_only
3856
 *   If this is set to TRUE, the function will perform the access checks and
3857
 *   return the site offline status, but not log the user out or display any
3858
 *   messages.
3859
 *
3860
 * @return
3861
 *   FALSE if the site is not in maintenance mode, the user login page is
3862
 *   displayed, or the user has the 'access site in maintenance mode'
3863
 *   permission. TRUE for anonymous users not being on the login page when the
3864
 *   site is in maintenance mode.
3865
 */
3866
function _menu_site_is_offline($check_only = FALSE) {
3867
  // Check if site is in maintenance mode.
3868
  if (variable_get('maintenance_mode', 0)) {
3869
    if (user_access('access site in maintenance mode')) {
3870
      // Ensure that the maintenance mode message is displayed only once
3871
      // (allowing for page redirects) and specifically suppress its display on
3872
      // the maintenance mode settings page.
3873
      if (!$check_only && $_GET['q'] != 'admin/config/development/maintenance') {
3874
        if (user_access('administer site configuration')) {
3875
          drupal_set_message(t('Operating in maintenance mode. <a href="@url">Go online.</a>', array('@url' => url('admin/config/development/maintenance'))), 'status', FALSE);
3876
        }
3877
        else {
3878
          drupal_set_message(t('Operating in maintenance mode.'), 'status', FALSE);
3879
        }
3880
      }
3881
    }
3882
    else {
3883
      return TRUE;
3884
    }
3885
  }
3886
  return FALSE;
3887
}
3888

    
3889
/**
3890
 * @} End of "defgroup menu".
3891
 */