Simpletest Coverage - includes/menu.inc

1 <?php
2 // $Id: menu.inc,v 1.333 2009/08/11 17:26:33 webchick Exp $
3
4 /**
5 * @file
6 * API for the Drupal menu system.
7 */
8
9 /**
10 * @defgroup menu Menu system
11 * @{
12 * Define the navigation menus, and route page requests to code based on URLs.
13 *
14 * The Drupal menu system drives both the navigation system from a user
15 * perspective and the callback system that Drupal uses to respond to URLs
16 * passed from the browser. For this reason, a good understanding of the
17 * menu system is fundamental to the creation of complex modules.
18 *
19 * Drupal's menu system follows a simple hierarchy defined by paths.
20 * Implementations of hook_menu() define menu items and assign them to
21 * paths (which should be unique). The menu system aggregates these items
22 * and determines the menu hierarchy from the paths. For example, if the
23 * paths defined were a, a/b, e, a/b/c/d, f/g, and a/b/h, the menu system
24 * would form the structure:
25 * - a
26 * - a/b
27 * - a/b/c/d
28 * - a/b/h
29 * - e
30 * - f/g
31 * Note that the number of elements in the path does not necessarily
32 * determine the depth of the menu item in the tree.
33 *
34 * When responding to a page request, the menu system looks to see if the
35 * path requested by the browser is registered as a menu item with a
36 * callback. If not, the system searches up the menu tree for the most
37 * complete match with a callback it can find. If the path a/b/i is
38 * requested in the tree above, the callback for a/b would be used.
39 *
40 * The found callback function is called with any arguments specified
41 * in the "page arguments" attribute of its menu item. The
42 * attribute must be an array. After these arguments, any remaining
43 * components of the path are appended as further arguments. In this
44 * way, the callback for a/b above could respond to a request for
45 * a/b/i differently than a request for a/b/j.
46 *
47 * For an illustration of this process, see page_example.module.
48 *
49 * Access to the callback functions is also protected by the menu system.
50 * The "access callback" with an optional "access arguments" of each menu
51 * item is called before the page callback proceeds. If this returns TRUE,
52 * then access is granted; if FALSE, then access is denied. Menu items may
53 * omit this attribute to use the value provided by an ancestor item.
54 *
55 * In the default Drupal interface, you will notice many links rendered as
56 * tabs. These are known in the menu system as "local tasks", and they are
57 * rendered as tabs by default, though other presentations are possible.
58 * Local tasks function just as other menu items in most respects. It is
59 * convention that the names of these tasks should be short verbs if
60 * possible. In addition, a "default" local task should be provided for
61 * each set. When visiting a local task's parent menu item, the default
62 * local task will be rendered as if it is selected; this provides for a
63 * normal tab user experience. This default task is special in that it
64 * links not to its provided path, but to its parent item's path instead.
65 * The default task's path is only used to place it appropriately in the
66 * menu hierarchy.
67 *
68 * Everything described so far is stored in the menu_router table. The
69 * menu_links table holds the visible menu links. By default these are
70 * derived from the same hook_menu definitions, however you are free to
71 * add more with menu_link_save().
72 */
73
74 /**
75 * @name Menu flags
76 * @{
77 * Flags for use in the "type" attribute of menu items.
78 */
79
80 /**
81 * Internal menu flag -- menu item is the root of the menu tree.
82 */
83 define('MENU_IS_ROOT', 0x0001);
84
85 /**
86 * Internal menu flag -- menu item is visible in the menu tree.
87 */
88 define('MENU_VISIBLE_IN_TREE', 0x0002);
89
90 /**
91 * Internal menu flag -- menu item is visible in the breadcrumb.
92 */
93 define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004);
94
95 /**
96 * Internal menu flag -- menu item links back to its parnet.
97 */
98 define('MENU_LINKS_TO_PARENT', 0x0008);
99
100 /**
101 * Internal menu flag -- menu item can be modified by administrator.
102 */
103 define('MENU_MODIFIED_BY_ADMIN', 0x0020);
104
105 /**
106 * Internal menu flag -- menu item was created by administrator.
107 */
108 define('MENU_CREATED_BY_ADMIN', 0x0040);
109
110 /**
111 * Internal menu flag -- menu item is a local task.
112 */
113 define('MENU_IS_LOCAL_TASK', 0x0080);
114
115 /**
116 * @} End of "Menu flags".
117 */
118
119 /**
120 * @name Menu item types
121 * @{
122 * Menu item definitions provide one of these constants, which are shortcuts for
123 * combinations of the above flags.
124 */
125
126 /**
127 * Menu type -- A "normal" menu item that's shown in menu and breadcrumbs.
128 *
129 * Normal menu items show up in the menu tree and can be moved/hidden by
130 * the administrator. Use this for most menu items. It is the default value if
131 * no menu item type is specified.
132 */
133 define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);
134
135 /**
136 * Menu type -- A hidden, internal callback, typically used for API calls.
137 *
138 * Callbacks simply register a path so that the correct function is fired
139 * when the URL is accessed. They are not shown in the menu.
140 */
141 define('MENU_CALLBACK', MENU_VISIBLE_IN_BREADCRUMB);
142
143 /**
144 * Menu type -- A normal menu item, hidden until enabled by an administrator.
145 *
146 * Modules may "suggest" menu items that the administrator may enable. They act
147 * just as callbacks do until enabled, at which time they act like normal items.
148 * Note for the value: 0x0010 was a flag which is no longer used, but this way
149 * the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate.
150 */
151 define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010);
152
153 /**
154 * Menu type -- A task specific to the parent item, usually rendered as a tab.
155 *
156 * Local tasks are menu items that describe actions to be performed on their
157 * parent item. An example is the path "node/52/edit", which performs the
158 * "edit" task on "node/52".
159 */
160 define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK);
161
162 /**
163 * Menu type -- The "default" local task, which is initially active.
164 *
165 * Every set of local tasks should provide one "default" task, that links to the
166 * same path as its parent when clicked.
167 */
168 define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT);
169
170 /**
171 * @} End of "Menu item types".
172 */
173
174 /**
175 * @name Menu status codes
176 * @{
177 * Status codes for menu callbacks.
178 */
179
180 /**
181 * Internal menu status code -- Menu item was found.
182 */
183 define('MENU_FOUND', 1);
184
185 /**
186 * Internal menu status code -- Menu item was not found.
187 */
188 define('MENU_NOT_FOUND', 2);
189
190 /**
191 * Internal menu status code -- Menu item access is denied.
192 */
193 define('MENU_ACCESS_DENIED', 3);
194
195 /**
196 * Internal menu status code -- Menu item inaccessible because site is offline.
197 */
198 define('MENU_SITE_OFFLINE', 4);
199
200 /**
201 * @} End of "Menu status codes".
202 */
203
204 /**
205 * @Name Menu tree parameters
206 * @{
207 * Menu tree
208 */
209
210 /**
211 * The maximum number of path elements for a menu callback
212 */
213 define('MENU_MAX_PARTS', 8);
214
215
216 /**
217 * The maximum depth of a menu links tree - matches the number of p columns.
218 */
219 define('MENU_MAX_DEPTH', 9);
220
221
222 /**
223 * @} End of "Menu tree parameters".
224 */
225
226 /**
227 * Returns the ancestors (and relevant placeholders) for any given path.
228 *
229 * For example, the ancestors of node/12345/edit are:
230 * - node/12345/edit
231 * - node/12345/%
232 * - node/%/edit
233 * - node/%/%
234 * - node/12345
235 * - node/%
236 * - node
237 *
238 * To generate these, we will use binary numbers. Each bit represents a
239 * part of the path. If the bit is 1, then it represents the original
240 * value while 0 means wildcard. If the path is node/12/edit/foo
241 * then the 1011 bitstring represents node/%/edit/foo where % means that
242 * any argument matches that part. We limit ourselves to using binary
243 * numbers that correspond the patterns of wildcards of router items that
244 * actually exists. This list of 'masks' is built in menu_rebuild().
245 *
246 * @param $parts
247 * An array of path parts, for the above example
248 * array('node', '12345', 'edit').
249 * @return
250 * An array which contains the ancestors and placeholders. Placeholders
251 * simply contain as many '%s' as the ancestors.
252 */
253 function menu_get_ancestors($parts) {
254 $number_parts = count($parts);
255 $ancestors = array();
256 $length = $number_parts - 1;
257 $end = (1 << $number_parts) - 1;
258 $masks = variable_get('menu_masks', array());
259 // Only examine patterns that actually exist as router items (the masks).
260 foreach ($masks as $i) {
261 if ($i > $end) {
262 // Only look at masks that are not longer than the path of interest.
263 continue;
264 }
265 elseif ($i < (1 << $length)) {
266 // We have exhausted the masks of a given length, so decrease the length.
267 --$length;
268 }
269 $current = '';
270 for ($j = $length; $j >= 0; $j--) {
271 if ($i & (1 << $j)) {
272 $current .= $parts[$length - $j];
273 }
274 else {
275 $current .= '%';
276 }
277 if ($j) {
278 $current .= '/';
279 }
280 }
281 $ancestors[] = $current;
282 }
283 return $ancestors;
284 }
285
286 /**
287 * The menu system uses serialized arrays stored in the database for
288 * arguments. However, often these need to change according to the
289 * current path. This function unserializes such an array and does the
290 * necessary change.
291 *
292 * Integer values are mapped according to the $map parameter. For
293 * example, if unserialize($data) is array('view', 1) and $map is
294 * array('node', '12345') then 'view' will not be changed because
295 * it is not an integer, but 1 will as it is an integer. As $map[1]
296 * is '12345', 1 will be replaced with '12345'. So the result will
297 * be array('node_load', '12345').
298 *
299 * @param @data
300 * A serialized array.
301 * @param @map
302 * An array of potential replacements.
303 * @return
304 * The $data array unserialized and mapped.
305 */
306 function menu_unserialize($data, $map) {
307 if ($data = unserialize($data)) {
308 foreach ($data as $k => $v) {
309 if (is_int($v)) {
310 $data[$k] = isset($map[$v]) ? $map[$v] : '';
311 }
312 }
313 return $data;
314 }
315 else {
316 return array();
317 }
318 }
319
320
321
322 /**
323 * Replaces the statically cached item for a given path.
324 *
325 * @param $path
326 * The path.
327 * @param $router_item
328 * The router item. Usually you take a router entry from menu_get_item and
329 * set it back either modified or to a different path. This lets you modify the
330 * navigation block, the page title, the breadcrumb and the page help in one
331 * call.
332 */
333 function menu_set_item($path, $router_item) {
334 menu_get_item($path, $router_item);
335 }
336
337 /**
338 * Get a router item.
339 *
340 * @param $path
341 * The path, for example node/5. The function will find the corresponding
342 * node/% item and return that.
343 * @param $router_item
344 * Internal use only.
345 * @return
346 * The router item, an associate array corresponding to one row in the
347 * menu_router table. The value of key map holds the loaded objects. The
348 * value of key access is TRUE if the current user can access this page.
349 * The values for key title, page_arguments, access_arguments will be
350 * filled in based on the database values and the objects loaded.
351 */
352 function menu_get_item($path = NULL, $router_item = NULL) {
353 $router_items = &drupal_static(__FUNCTION__);
354 if (!isset($path)) {
355 $path = $_GET['q'];
356 }
357 if (isset($router_item)) {
358 $router_items[$path] = $router_item;
359 }
360 if (!isset($router_items[$path])) {
361 $original_map = arg(NULL, $path);
362 $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
363 $ancestors = menu_get_ancestors($parts);
364 $router_item = db_select('menu_router')
365 ->fields('menu_router')
366 ->condition('path', $ancestors, 'IN')
367 ->orderBy('fit', 'DESC')
368 ->range(0, 1)
369 ->addTag('menu_get_item')
370 ->execute()->fetchAssoc();
371 if ($router_item) {
372 $map = _menu_translate($router_item, $original_map);
373 if ($map === FALSE) {
374 $router_items[$path] = FALSE;
375 return FALSE;
376 }
377 if ($router_item['access']) {
378 $router_item['map'] = $map;
379 $router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $map), array_slice($map, $router_item['number_parts']));
380 }
381 }
382 $router_items[$path] = $router_item;
383 }
384 return $router_items[$path];
385 }
386
387 /**
388 * Execute the page callback associated with the current path
389 */
390 function menu_execute_active_handler($path = NULL) {
391 if (_menu_site_is_offline()) {
392 return MENU_SITE_OFFLINE;
393 }
394 // Rebuild if we know it's needed, or if the menu masks are missing which
395 // occurs rarely, likely due to a race condition of multiple rebuilds.
396 if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) {
397 menu_rebuild();
398 }
399 if ($router_item = menu_get_item($path)) {
400 if ($router_item['access']) {
401 if (drupal_function_exists($router_item['page_callback'])) {
402 return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
403 }
404 }
405 else {
406 return MENU_ACCESS_DENIED;
407 }
408 }
409 return MENU_NOT_FOUND;
410 }
411
412 /**
413 * Loads objects into the map as defined in the $item['load_functions'].
414 *
415 * @param $item
416 * A menu router or menu link item
417 * @param $map
418 * An array of path arguments (ex: array('node', '5'))
419 * @return
420 * Returns TRUE for success, FALSE if an object cannot be loaded.
421 * Names of object loading functions are placed in $item['load_functions'].
422 * Loaded objects are placed in $map[]; keys are the same as keys in the
423 * $item['load_functions'] array.
424 * $item['access'] is set to FALSE if an object cannot be loaded.
425 */
426 function _menu_load_objects(&$item, &$map) {
427 if ($load_functions = $item['load_functions']) {
428 // If someone calls this function twice, then unserialize will fail.
429 if ($load_functions_unserialized = unserialize($load_functions)) {
430 $load_functions = $load_functions_unserialized;
431 }
432 $path_map = $map;
433 foreach ($load_functions as $index => $function) {
434 if ($function) {
435 $value = isset($path_map[$index]) ? $path_map[$index] : '';
436 if (is_array($function)) {
437 // Set up arguments for the load function. These were pulled from
438 // 'load arguments' in the hook_menu() entry, but they need
439 // some processing. In this case the $function is the key to the
440 // load_function array, and the value is the list of arguments.
441 list($function, $args) = each($function);
442 $load_functions[$index] = $function;
443
444 // Some arguments are placeholders for dynamic items to process.
445 foreach ($args as $i => $arg) {
446 if ($arg === '%index') {
447 // Pass on argument index to the load function, so multiple
448 // occurrences of the same placeholder can be identified.
449 $args[$i] = $index;
450 }
451 if ($arg === '%map') {
452 // Pass on menu map by reference. The accepting function must
453 // also declare this as a reference if it wants to modify
454 // the map.
455 $args[$i] = &$map;
456 }
457 if (is_int($arg)) {
458 $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : '';
459 }
460 }
461 array_unshift($args, $value);
462 $return = call_user_func_array($function, $args);
463 }
464 else {
465 $return = $function($value);
466 }
467 // If callback returned an error or there is no callback, trigger 404.
468 if ($return === FALSE) {
469 $item['access'] = FALSE;
470 $map = FALSE;
471 return FALSE;
472 }
473 $map[$index] = $return;
474 }
475 }
476 $item['load_functions'] = $load_functions;
477 }
478 return TRUE;
479 }
480
481 /**
482 * Check access to a menu item using the access callback
483 *
484 * @param $item
485 * A menu router or menu link item
486 * @param $map
487 * An array of path arguments (ex: array('node', '5'))
488 * @return
489 * $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
490 */
491 function _menu_check_access(&$item, $map) {
492 // Determine access callback, which will decide whether or not the current
493 // user has access to this path.
494 $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']);
495 // Check for a TRUE or FALSE value.
496 if (is_numeric($callback)) {
497 $item['access'] = (bool)$callback;
498 }
499 else {
500 $arguments = menu_unserialize($item['access_arguments'], $map);
501 // As call_user_func_array is quite slow and user_access is a very common
502 // callback, it is worth making a special case for it.
503 if ($callback == 'user_access') {
504 $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
505 }
506 elseif (drupal_function_exists($callback)) {
507 $item['access'] = call_user_func_array($callback, $arguments);
508 }
509 }
510 }
511
512 /**
513 * Localize the router item title using t() or another callback.
514 *
515 * Translate the title and description to allow storage of English title
516 * strings in the database, yet display of them in the language required
517 * by the current user.
518 *
519 * @param $item
520 * A menu router item or a menu link item.
521 * @param $map
522 * The path as an array with objects already replaced. E.g., for path
523 * node/123 $map would be array('node', $node) where $node is the node
524 * object for node 123.
525 * @param $link_translate
526 * TRUE if we are translating a menu link item; FALSE if we are
527 * translating a menu router item.
528 * @return
529 * No return value.
530 * $item['title'] is localized according to $item['title_callback'].
531 * If an item's callback is check_plain(), $item['options']['html'] becomes
532 * TRUE.
533 * $item['description'] is translated using t().
534 * When doing link translation and the $item['options']['attributes']['title']
535 * (link title attribute) matches the description, it is translated as well.
536 */
537 function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
538 $callback = $item['title_callback'];
539 $item['localized_options'] = $item['options'];
540 // If we are translating the title of a menu link, and its title is the same
541 // as the corresponding router item, then we can use the title information
542 // from the router. If it's customized, then we need to use the link title
543 // itself; can't localize.
544 // If we are translating a router item (tabs, page, breadcrumb), then we
545 // can always use the information from the router item.
546 if (!$link_translate || ($item['title'] == $item['link_title'])) {
547 // t() is a special case. Since it is used very close to all the time,
548 // we handle it directly instead of using indirect, slower methods.
549 if ($callback == 't') {
550 if (empty($item['title_arguments'])) {
551 $item['title'] = t($item['title']);
552 }
553 else {
554 $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
555 }
556 }
557 elseif ($callback && drupal_function_exists($callback)) {
558 if (empty($item['title_arguments'])) {
559 $item['title'] = $callback($item['title']);
560 }
561 else {
562 $item['title'] = call_user_func_array($callback, menu_unserialize($item['title_arguments'], $map));
563 }
564 // Avoid calling check_plain again on l() function.
565 if ($callback == 'check_plain') {
566 $item['localized_options']['html'] = TRUE;
567 }
568 }
569 }
570 elseif ($link_translate) {
571 $item['title'] = $item['link_title'];
572 }
573
574 // Translate description, see the motivation above.
575 if (!empty($item['description'])) {
576 $original_description = $item['description'];
577 $item['description'] = t($item['description']);
578 if ($link_translate && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) {
579 $item['localized_options']['attributes']['title'] = $item['description'];
580 }
581 }
582 }
583
584 /**
585 * Handles dynamic path translation and menu access control.
586 *
587 * When a user arrives on a page such as node/5, this function determines
588 * what "5" corresponds to, by inspecting the page's menu path definition,
589 * node/%node. This will call node_load(5) to load the corresponding node
590 * object.
591 *
592 * It also works in reverse, to allow the display of tabs and menu items which
593 * contain these dynamic arguments, translating node/%node to node/5.
594 *
595 * Translation of menu item titles and descriptions are done here to
596 * allow for storage of English strings in the database, and translation
597 * to the language required to generate the current page.
598 *
599 * @param $router_item
600 * A menu router item
601 * @param $map
602 * An array of path arguments (ex: array('node', '5'))
603 * @param $to_arg
604 * Execute $item['to_arg_functions'] or not. Use only if you want to render a
605 * path from the menu table, for example tabs.
606 * @return
607 * Returns the map with objects loaded as defined in the
608 * $item['load_functions']. $item['access'] becomes TRUE if the item is
609 * accessible, FALSE otherwise. $item['href'] is set according to the map.
610 * If an error occurs during calling the load_functions (like trying to load
611 * a non existing node) then this function return FALSE.
612 */
613 function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
614 if ($to_arg) {
615 // Fill in missing path elements, such as the current uid.
616 _menu_link_map_translate($map, $router_item['to_arg_functions']);
617 }
618 // The $path_map saves the pieces of the path as strings, while elements in
619 // $map may be replaced with loaded objects.
620 $path_map = $map;
621 if (!_menu_load_objects($router_item, $map)) {
622 // An error occurred loading an object.
623 $router_item['access'] = FALSE;
624 return FALSE;
625 }
626
627 // Generate the link path for the page request or local tasks.
628 $link_map = explode('/', $router_item['path']);
629 for ($i = 0; $i < $router_item['number_parts']; $i++) {
630 if ($link_map[$i] == '%') {
631 $link_map[$i] = $path_map[$i];
632 }
633 }
634 $router_item['href'] = implode('/', $link_map);
635 $router_item['options'] = array();
636 _menu_check_access($router_item, $map);
637
638 // For performance, don't localize an item the user can't access.
639 if ($router_item['access']) {
640 _menu_item_localize($router_item, $map);
641 }
642
643 return $map;
644 }
645
646 /**
647 * This function translates the path elements in the map using any to_arg
648 * helper function. These functions take an argument and return an object.
649 * See http://drupal.org/node/109153 for more information.
650 *
651 * @param $map
652 * An array of path arguments (ex: array('node', '5'))
653 * @param $to_arg_functions
654 * An array of helper function (ex: array(2 => 'menu_tail_to_arg'))
655 */
656 function _menu_link_map_translate(&$map, $to_arg_functions) {
657 if ($to_arg_functions) {
658 $to_arg_functions = unserialize($to_arg_functions);
659 foreach ($to_arg_functions as $index => $function) {
660 // Translate place-holders into real values.
661 $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
662 if (!empty($map[$index]) || isset($arg)) {
663 $map[$index] = $arg;
664 }
665 else {
666 unset($map[$index]);
667 }
668 }
669 }
670 }
671
672 function menu_tail_to_arg($arg, $map, $index) {
673 return implode('/', array_slice($map, $index));
674 }
675
676 /**
677 * This function is similar to _menu_translate() but does link-specific
678 * preparation such as always calling to_arg functions
679 *
680 * @param $item
681 * A menu link
682 * @return
683 * Returns the map of path arguments with objects loaded as defined in the
684 * $item['load_functions'].
685 * $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
686 * $item['href'] is generated from link_path, possibly by to_arg functions.
687 * $item['title'] is generated from link_title, and may be localized.
688 * $item['options'] is unserialized; it is also changed within the call here
689 * to $item['localized_options'] by _menu_item_localize().
690 */
691 function _menu_link_translate(&$item) {
692 $item['options'] = unserialize($item['options']);
693 if ($item['external']) {
694 $item['access'] = 1;
695 $map = array();
696 $item['href'] = $item['link_path'];
697 $item['title'] = $item['link_title'];
698 $item['localized_options'] = $item['options'];
699 }
700 else {
701 $map = explode('/', $item['link_path']);
702 _menu_link_map_translate($map, $item['to_arg_functions']);
703 $item['href'] = implode('/', $map);
704
705 // Note - skip callbacks without real values for their arguments.
706 if (strpos($item['href'], '%') !== FALSE) {
707 $item['access'] = FALSE;
708 return FALSE;
709 }
710 // menu_tree_check_access() may set this ahead of time for links to nodes.
711 if (!isset($item['access'])) {
712 if (!_menu_load_objects($item, $map)) {
713 // An error occurred loading an object.
714 $item['access'] = FALSE;
715 return FALSE;
716 }
717 _menu_check_access($item, $map);
718 }
719 // For performance, don't localize a link the user can't access.
720 if ($item['access']) {
721 _menu_item_localize($item, $map, TRUE);
722 }
723 }
724
725 // Allow other customizations - e.g. adding a page-specific query string to the
726 // options array. For performance reasons we only invoke this hook if the link
727 // has the 'alter' flag set in the options array.
728 if (!empty($item['options']['alter'])) {
729 drupal_alter('translated_menu_link', $item, $map);
730 }
731
732 return $map;
733 }
734
735 /**
736 * Get a loaded object from a router item.
737 *
738 * menu_get_object() provides access to objects loaded by the current router
739 * item. For example, on the page node/%node, the router loads the %node object,
740 * and calling menu_get_object() will return that. Normally, it is necessary to
741 * specify the type of object referenced, however node is the default.
742 * The following example tests to see whether the node being displayed is of the
743 * "story" content type:
744 * @code
745 * $node = menu_get_object();
746 * $story = $node->type == 'story';
747 * @endcode
748 *
749 * @param $type
750 * Type of the object. These appear in hook_menu definitions as %type. Core
751 * provides aggregator_feed, aggregator_category, contact, filter_format,
752 * forum_term, menu, menu_link, node, taxonomy_vocabulary, user. See the
753 * relevant {$type}_load function for more on each. Defaults to node.
754 * @param $position
755 * The position of the object in the path, where the first path segment is 0.
756 * For node/%node, the position of %node is 1, but for comment/reply/%node,
757 * it's 2. Defaults to 1.
758 * @param $path
759 * See menu_get_item() for more on this. Defaults to the current path.
760 */
761 function menu_get_object($type = 'node', $position = 1, $path = NULL) {
762 $router_item = menu_get_item($path);
763 if (isset($router_item['load_functions'][$position]) && !empty($router_item['map'][$position]) && $router_item['load_functions'][$position] == $type . '_load') {
764 return $router_item['map'][$position];
765 }
766 }
767
768 /**
769 * Render a menu tree based on the current path.
770 *
771 * The tree is expanded based on the current path and dynamic paths are also
772 * changed according to the defined to_arg functions (for example the 'My account'
773 * link is changed from user/% to a link with the current user's uid).
774 *
775 * @param $menu_name
776 * The name of the menu.
777 * @return
778 * The rendered HTML of that menu on the current page.
779 */
780 function menu_tree($menu_name) {
781 $menu_output = &drupal_static(__FUNCTION__, array());
782
783 if (!isset($menu_output[$menu_name])) {
784 $tree = menu_tree_page_data($menu_name);
785 $menu_output[$menu_name] = menu_tree_output($tree);
786 }
787 return $menu_output[$menu_name];
788 }
789
790 /**
791 * Returns a rendered menu tree.
792 *
793 * @param $tree
794 * A data structure representing the tree as returned from menu_tree_data.
795 * @return
796 * The rendered HTML of that data structure.
797 */
798 function menu_tree_output($tree) {
799 $output = '';
800 $items = array();
801
802 // Pull out just the menu items we are going to render so that we
803 // get an accurate count for the first/last classes.
804 foreach ($tree as $data) {
805 if (!$data['link']['hidden']) {
806 $items[] = $data;
807 }
808 }
809
810 $num_items = count($items);
811 foreach ($items as $i => $data) {
812 $extra_class = array();
813 if ($i == 0) {
814 $extra_class[] = 'first';
815 }
816 if ($i == $num_items - 1) {
817 $extra_class[] = 'last';
818 }
819 $extra_class = implode(' ', $extra_class);
820 $link = theme('menu_item_link', $data['link']);
821 if ($data['below']) {
822 $output .= theme('menu_item', $link, $data['link']['has_children'], menu_tree_output($data['below']), $data['link']['in_active_trail'], $extra_class);
823 }
824 else {
825 $output .= theme('menu_item', $link, $data['link']['has_children'], '', $data['link']['in_active_trail'], $extra_class);
826 }
827 }
828 return $output ? theme('menu_tree', $output) : '';
829 }
830
831 /**
832 * Get the data structure representing a named menu tree.
833 *
834 * Since this can be the full tree including hidden items, the data returned
835 * may be used for generating an an admin interface or a select.
836 *
837 * @param $menu_name
838 * The named menu links to return
839 * @param $item
840 * A fully loaded menu link, or NULL. If a link is supplied, only the
841 * path to root will be included in the returned tree- as if this link
842 * represented the current page in a visible menu.
843 * @return
844 * An tree of menu links in an array, in the order they should be rendered.
845 */
846 function menu_tree_all_data($menu_name, $item = NULL) {
847 $tree = &drupal_static(__FUNCTION__, array());
848
849 // Use $mlid as a flag for whether the data being loaded is for the whole tree.
850 $mlid = isset($item['mlid']) ? $item['mlid'] : 0;
851 // Generate a cache ID (cid) specific for this $menu_name and $item.
852 $cid = 'links:' . $menu_name . ':all-cid:' . $mlid;
853
854 if (!isset($tree[$cid])) {
855 // If the static variable doesn't have the data, check {cache_menu}.
856 $cache = cache_get($cid, 'cache_menu');
857 if ($cache && isset($cache->data)) {
858 // If the cache entry exists, it will just be the cid for the actual data.
859 // This avoids duplication of large amounts of data.
860 $cache = cache_get($cache->data, 'cache_menu');
861 if ($cache && isset($cache->data)) {
862 $data = $cache->data;
863 }
864 }
865 // If the tree data was not in the cache, $data will be NULL.
866 if (!isset($data)) {
867 // Build and run the query, and build the tree.
868 $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
869 $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
870 $query->fields('ml');
871 $query->fields('m', array(
872 'load_functions',
873 'to_arg_functions',
874 'access_callback',
875 'access_arguments',
876 'page_callback',
877 'page_arguments',
878 'title',
879 'title_callback',
880 'title_arguments',
881 'type',
882 'description',
883 ));
884 for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
885 $query->orderBy('p' . $i, 'ASC');
886 }
887 $query->condition('ml.menu_name', $menu_name);
888
889 if ($mlid) {
890 // The tree is for a single item, so we need to match the values in its
891 // p columns and 0 (the top level) with the plid values of other links.
892 $args = array(0);
893 for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
894 $args[] = $item["p$i"];
895 }
896 $args = array_unique($args);
897 $query->condition('ml.plid', $args, 'IN');
898 $parents = $args;
899 $parents[] = $item['mlid'];
900 }
901 else {
902 // Get all links in this menu.
903 $parents = array();
904 }
905 // Select the links from the table, and recursively build the tree. We
906 // LEFT JOIN since there is no match in {menu_router} for an external
907 // link.
908 $data['tree'] = menu_tree_data($query->execute(), $parents);
909 $data['node_links'] = array();
910 menu_tree_collect_node_links($data['tree'], $data['node_links']);
911 // Cache the data, if it is not already in the cache.
912 $tree_cid = _menu_tree_cid($menu_name, $data);
913 if (!cache_get($tree_cid, 'cache_menu')) {
914 cache_set($tree_cid, $data, 'cache_menu');
915 }
916 // Cache the cid of the (shared) data using the menu and item-specific cid.
917 cache_set($cid, $tree_cid, 'cache_menu');
918 }
919 // Check access for the current user to each item in the tree.
920 menu_tree_check_access($data['tree'], $data['node_links']);
921 $tree[$cid] = $data['tree'];
922 }
923
924 return $tree[$cid];
925 }
926
927 /**
928 * Get the data structure representing a named menu tree, based on the current page.
929 *
930 * The tree order is maintained by storing each parent in an individual
931 * field, see http://drupal.org/node/141866 for more.
932 *
933 * @param $menu_name
934 * The named menu links to return
935 * @return
936 * An array of menu links, in the order they should be rendered. The array
937 * is a list of associative arrays -- these have two keys, link and below.
938 * link is a menu item, ready for theming as a link. Below represents the
939 * submenu below the link if there is one, and it is a subtree that has the
940 * same structure described for the top-level array.
941 */
942 function menu_tree_page_data($menu_name) {
943 $tree = &drupal_static(__FUNCTION__, array());
944
945 // Load the menu item corresponding to the current page.
946 if ($item = menu_get_item()) {
947 // Generate a cache ID (cid) specific for this page.
948 $cid = 'links:' . $menu_name . ':page-cid:' . $item['href'] . ':' . (int)$item['access'];
949
950 if (!isset($tree[$cid])) {
951 // If the static variable doesn't have the data, check {cache_menu}.
952 $cache = cache_get($cid, 'cache_menu');
953 if ($cache && isset($cache->data)) {
954 // If the cache entry exists, it will just be the cid for the actual data.
955 // This avoids duplication of large amounts of data.
956 $cache = cache_get($cache->data, 'cache_menu');
957 if ($cache && isset($cache->data)) {
958 $data = $cache->data;
959 }
960 }
961 // If the tree data was not in the cache, $data will be NULL.
962 if (!isset($data)) {
963 // Build and run the query, and build the tree.
964 if ($item['access']) {
965 // Check whether a menu link exists that corresponds to the current path.
966 $args[] = $item['href'];
967 if (drupal_is_front_page()) {
968 $args[] = '<front>';
969 }
970 $parents = db_select('menu_links')
971 ->fields('menu_links', array(
972 'p1',
973 'p2',
974 'p3',
975 'p4',
976 'p5',
977 'p6',
978 'p7',
979 'p8',
980 ))
981 ->condition('menu_name', $menu_name)
982 ->condition('link_path', $args, 'IN')
983 ->execute()->fetchAssoc();
984
985 if (empty($parents)) {
986 // If no link exists, we may be on a local task that's not in the links.
987 // TODO: Handle the case like a local task on a specific node in the menu.
988 $parents = db_select('menu_links')
989 ->fields('menu_links', array(
990 'p1',
991 'p2',
992 'p3',
993 'p4',
994 'p5',
995 'p6',
996 'p7',
997 'p8',
998 ))
999 ->condition('menu_name', $menu_name)
1000 ->condition('link_path', $item['tab_root'])
1001 ->execute()->fetchAssoc();
1002 }
1003 // We always want all the top-level links with plid == 0.
1004 $parents[] = '0';
1005
1006 // Use array_values() so that the indices are numeric for array_merge().
1007 $args = $parents = array_unique(array_values($parents));
1008 $expanded = variable_get('menu_expanded', array());
1009 // Check whether the current menu has any links set to be expanded.
1010 if (in_array($menu_name, $expanded)) {
1011 // Collect all the links set to be expanded, and then add all of
1012 // their children to the list as well.
1013 do {
1014 $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC))
1015 ->fields('menu_links', array('mlid'))
1016 ->condition('menu_name', $menu_name)
1017 ->condition('expanded', 1)
1018 ->condition('has_children', 1)
1019 ->condition('plid', $args, 'IN')
1020 ->condition('mlid', $args, 'NOT IN')
1021 ->execute();
1022 $num_rows = FALSE;
1023 foreach ($result as $item) {
1024 $args[] = $item['mlid'];
1025 $num_rows = TRUE;
1026 }
1027 } while ($num_rows);
1028 }
1029 }
1030 else {
1031 // Show only the top-level menu items when access is denied.
1032 $args = array(0);
1033 $parents = array();
1034 }
1035 // Select the links from the table, and recursively build the tree. We
1036 // LEFT JOIN since there is no match in {menu_router} for an external
1037 // link.
1038 $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
1039 $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
1040 $query->fields('ml');
1041 $query->fields('m', array(
1042 'load_functions',
1043 'to_arg_functions',
1044 'access_callback',
1045 'access_arguments',
1046 'page_callback',
1047 'page_arguments',
1048 'title',
1049 'title_callback',
1050 'title_arguments',
1051 'type',
1052 'description',
1053 ));
1054 for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
1055 $query->orderBy('p' . $i, 'ASC');
1056 }
1057 $query->condition('ml.menu_name', $menu_name);
1058 $query->condition('ml.plid', $args, 'IN');
1059 $data['tree'] = menu_tree_data($query->execute(), $parents);
1060 $data['node_links'] = array();
1061 menu_tree_collect_node_links($data['tree'], $data['node_links']);
1062 // Cache the data, if it is not already in the cache.
1063 $tree_cid = _menu_tree_cid($menu_name, $data);
1064 if (!cache_get($tree_cid, 'cache_menu')) {
1065 cache_set($tree_cid, $data, 'cache_menu');
1066 }
1067 // Cache the cid of the (shared) data using the page-specific cid.
1068 cache_set($cid, $tree_cid, 'cache_menu');
1069 }
1070 // Check access for the current user to each item in the tree.
1071 menu_tree_check_access($data['tree'], $data['node_links']);
1072 $tree[$cid] = $data['tree'];
1073 }
1074 return $tree[$cid];
1075 }
1076
1077 return array();
1078 }
1079
1080 /**
1081 * Helper function - compute the real cache ID for menu tree data.
1082 */
1083 function _menu_tree_cid($menu_name, $data) {
1084 return 'links:' . $menu_name . ':tree-data:' . md5(serialize($data));
1085 }
1086
1087 /**
1088 * Recursive helper function - collect node links.
1089 *
1090 * @param $tree
1091 * The menu tree you wish to collect node links from.
1092 * @param $node_links
1093 * An array in which to store the collected node links.
1094 */
1095 function menu_tree_collect_node_links(&$tree, &$node_links) {
1096 foreach ($tree as $key => $v) {
1097 if ($tree[$key]['link']['router_path'] == 'node/%') {
1098 $nid = substr($tree[$key]['link']['link_path'], 5);
1099 if (is_numeric($nid)) {
1100 $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link'];
1101 $tree[$key]['link']['access'] = FALSE;
1102 }
1103 }
1104 if ($tree[$key]['below']) {
1105 menu_tree_collect_node_links($tree[$key]['below'], $node_links);
1106 }
1107 }
1108 }
1109
1110 /**
1111 * Check access and perform other dynamic operations for each link in the tree.
1112 *
1113 * @param $tree
1114 * The menu tree you wish to operate on.
1115 * @param $node_links
1116 * A collection of node link references generated from $tree by
1117 * menu_tree_collect_node_links().
1118 */
1119 function menu_tree_check_access(&$tree, $node_links = array()) {
1120
1121 if ($node_links) {
1122 $nids = array_keys($node_links);
1123 $select = db_select('node');
1124 $select->addField('node', 'nid');
1125 $select->condition('status', 1);
1126 $select->condition('nid', $nids, 'IN');
1127 $select->addTag('node_access');
1128 $nids = $select->execute()->fetchCol();
1129 foreach ($nids as $nid) {
1130 foreach ($node_links[$nid] as $mlid => $link) {
1131 $node_links[$nid][$mlid]['access'] = TRUE;
1132 }
1133 }
1134 }
1135 _menu_tree_check_access($tree);
1136 return;
1137 }
1138
1139 /**
1140 * Recursive helper function for menu_tree_check_access()
1141 */
1142 function _menu_tree_check_access(&$tree) {
1143 $new_tree = array();
1144 foreach ($tree as $key => $v) {
1145 $item = &$tree[$key]['link'];
1146 _menu_link_translate($item);
1147 if ($item['access']) {
1148 if ($tree[$key]['below']) {
1149 _menu_tree_check_access($tree[$key]['below']);
1150 }
1151 // The weights are made a uniform 5 digits by adding 50000 as an offset.
1152 // After _menu_link_translate(), $item['title'] has the localized link title.
1153 // Adding the mlid to the end of the index insures that it is unique.
1154 $new_tree[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $tree[$key];
1155 }
1156 }
1157 // Sort siblings in the tree based on the weights and localized titles.
1158 ksort($new_tree);
1159 $tree = $new_tree;
1160 }
1161
1162 /**
1163 * Build the data representing a menu tree.
1164 *
1165 * @param $result
1166 * The database result.
1167 * @param $parents
1168 * An array of the plid values that represent the path from the current page
1169 * to the root of the menu tree.
1170 * @param $depth
1171 * The depth of the current menu tree.
1172 * @return
1173 * See menu_tree_page_data for a description of the data structure.
1174 */
1175 function menu_tree_data($result = NULL, $parents = array(), $depth = 1) {
1176 list(, $tree) = _menu_tree_data($result, $parents, $depth);
1177 return $tree;
1178 }
1179
1180 /**
1181 * Recursive helper function to build the data representing a menu tree.
1182 *
1183 * The function is a bit complex because the rendering of an item depends on
1184 * the next menu item. So we are always rendering the element previously
1185 * processed not the current one.
1186 */
1187 function _menu_tree_data($result, $parents, $depth, $previous_element = '') {
1188 $remnant = NULL;
1189 $tree = array();
1190 foreach ($result as $item) {
1191 // We need to determine if we're on the path to root so we can later build
1192 // the correct active trail and breadcrumb.
1193 $item['in_active_trail'] = in_array($item['mlid'], $parents);
1194 // The current item is the first in a new submenu.
1195 if ($item['depth'] > $depth) {
1196 // _menu_tree returns an item and the menu tree structure.
1197 list($item, $below) = _menu_tree_data($result, $parents, $item['depth'], $item);
1198 if ($previous_element) {
1199 $tree[$previous_element['mlid']] = array(
1200 'link' => $previous_element,
1201 'below' => $below,
1202 );
1203 }
1204 else {
1205 $tree = $below;
1206 }
1207 // We need to fall back one level.
1208 if (!isset($item) || $item['depth'] < $depth) {
1209 return array($item, $tree);
1210 }
1211 // This will be the link to be output in the next iteration.
1212 $previous_element = $item;
1213 }
1214 // We are at the same depth, so we use the previous element.
1215 elseif ($item['depth'] == $depth) {
1216 if ($previous_element) {
1217 // Only the first time.
1218 $tree[$previous_element['mlid']] = array(
1219 'link' => $previous_element,
1220 'below' => FALSE,
1221 );
1222 }
1223 // This will be the link to be output in the next iteration.
1224 $previous_element = $item;
1225 }
1226 // The submenu ended with the previous item, so pass back the current item.
1227 else {
1228 $remnant = $item;
1229 break;
1230 }
1231 }
1232 if ($previous_element) {
1233 // We have one more link dangling.
1234 $tree[$previous_element['mlid']] = array(
1235 'link' => $previous_element,
1236 'below' => FALSE,
1237 );
1238 }
1239 return array($remnant, $tree);
1240 }
1241
1242 /**
1243 * Generate the HTML output for a single menu link.
1244 *
1245 * @ingroup themeable
1246 */
1247 function theme_menu_item_link($link) {
1248 if (empty($link['localized_options'])) {
1249 $link['localized_options'] = array();
1250 }
1251
1252 return l($link['title'], $link['href'], $link['localized_options']);
1253 }
1254
1255 /**
1256 * Generate the HTML output for a menu tree
1257 *
1258 * @ingroup themeable
1259 */
1260 function theme_menu_tree($tree) {
1261 return '<ul class="menu">' . $tree . '</ul>';
1262 }
1263
1264 /**
1265 * Generate the HTML output for a menu item and submenu.
1266 *
1267 * The menu item's LI element is given one of the following classes:
1268 * - expanded: The menu item is showing its submenu.
1269 * - collapsed: The menu item has a submenu which is not shown.
1270 * - leaf: The menu item has no submenu.
1271 *
1272 * @ingroup themeable
1273 *
1274 * @param $link
1275 * The fully-formatted link for this menu item.
1276 * @param $has_children
1277 * Boolean value indicating if this menu item has children.
1278 * @param $menu
1279 * Contains a fully-formatted submenu, if one exists for this menu item.
1280 * Defaults to NULL.
1281 * @param $in_active_trail
1282 * Boolean determining if the current page is below the menu item in the
1283 * menu system. Defaults to FALSE.
1284 * @param $extra_class
1285 * Extra classes that should be added to the class of the list item.
1286 * Defaults to NULL.
1287 */
1288 function theme_menu_item($link, $has_children, $menu = '', $in_active_trail = FALSE, $extra_class = NULL) {
1289 $class = ($menu ? 'expanded' : ($has_children ? 'collapsed' : 'leaf'));
1290 if (!empty($extra_class)) {
1291 $class .= ' ' . $extra_class;
1292 }
1293 if ($in_active_trail) {
1294 $class .= ' active-trail';
1295 }
1296 return '<li class="' . $class . '">' . $link . $menu . "</li>\n";
1297 }
1298
1299 /**
1300 * Generate the HTML output for a single local task link.
1301 *
1302 * @ingroup themeable
1303 */
1304 function theme_menu_local_task($link, $active = FALSE) {
1305 return '<li ' . ($active ? 'class="active" ' : '') . '>' . $link . "</li>\n";
1306 }
1307
1308 /**
1309 * Generates elements for the $arg array in the help hook.
1310 */
1311 function drupal_help_arg($arg = array()) {
1312 // Note - the number of empty elements should be > MENU_MAX_PARTS.
1313 return $arg + array('', '', '', '', '', '', '', '', '', '', '', '');
1314 }
1315
1316 /**
1317 * Returns the help associated with the active menu item.
1318 */
1319 function menu_get_active_help() {
1320 $output = '';
1321 $router_path = menu_tab_root_path();
1322 // We will always have a path unless we are on a 403 or 404.
1323 if (!$router_path) {
1324 return '';
1325 }
1326
1327 $arg = drupal_help_arg(arg(NULL));
1328 $empty_arg = drupal_help_arg();
1329
1330 foreach (module_implements('help') as $module) {
1331 $function = $module . '_help';
1332 // Lookup help for this path.
1333 if ($help = $function($router_path, $arg)) {
1334 $output .= $help . "\n";
1335 }
1336 // Add "more help" link on admin pages if the module provides a
1337 // standalone help page.
1338 if ($arg[0] == "admin" && user_access('access administration pages') && module_exists('help') && $function('admin/help#' . $arg[2], $empty_arg) && $help) {
1339 $output .= theme("more_help_link", url('admin/help/' . $arg[2]));
1340 }
1341 }
1342 return $output;
1343 }
1344
1345 /**
1346 * Build a list of named menus.
1347 */
1348 function menu_get_names() {
1349 $names = &drupal_static(__FUNCTION__);
1350
1351 if (empty($names)) {
1352 $names = db_select('menu_links')
1353 ->distinct()
1354 ->fields('menu_links', 'menu_name')
1355 ->orderBy('menu_name')
1356 ->execute()->fetchCol();
1357 }
1358 return $names;
1359 }
1360
1361 /**
1362 * Return an array containing the names of system-defined (default) menus.
1363 */
1364 function menu_list_system_menus() {
1365 return array('navigation' => 'Navigation', 'management' => 'Management', 'user-menu' => 'User menu', 'main-menu' => 'Main menu', 'secondary-menu' => 'Secondary menu');
1366 }
1367
1368 /**
1369 * Return an array of links to be rendered as the Main menu.
1370 */
1371 function menu_main_menu() {
1372 return menu_navigation_links(variable_get('menu_main_links_source', 'main-menu'));
1373 }
1374
1375 /**
1376 * Return an array of links to be rendered as the Secondary links.
1377 */
1378 function menu_secondary_menu() {
1379
1380 // If the secondary menu source is set as the primary menu, we display the
1381 // second level of the primary menu.
1382 if (variable_get('menu_secondary_links_source', 'user-menu') == variable_get('menu_main_links_source', 'main-menu')) {
1383 return menu_navigation_links(variable_get('menu_main_links_source', 'main-menu'), 1);
1384 }
1385 else {
1386 return menu_navigation_links(variable_get('menu_secondary_links_source', 'user-menu'), 0);
1387 }
1388 }
1389
1390 /**
1391 * Return an array of links for a navigation menu.
1392 *
1393 * @param $menu_name
1394 * The name of the menu.
1395 * @param $level
1396 * Optional, the depth of the menu to be returned.
1397 * @return
1398 * An array of links of the specified menu and level.
1399 */
1400 function menu_navigation_links($menu_name, $level = 0) {
1401 // Don't even bother querying the menu table if no menu is specified.
1402 if (empty($menu_name)) {
1403 return array();
1404 }
1405
1406 // Get the menu hierarchy for the current page.
1407 $tree = menu_tree_page_data($menu_name);
1408
1409 // Go down the active trail until the right level is reached.
1410 while ($level-- > 0 && $tree) {
1411 // Loop through the current level's items until we find one that is in trail.
1412 while ($item = array_shift($tree)) {
1413 if ($item['link']['in_active_trail']) {
1414 // If the item is in the active trail, we continue in the subtree.
1415 $tree = empty($item['below']) ? array() : $item['below'];
1416 break;
1417 }
1418 }
1419 }
1420
1421 // Create a single level of links.
1422 $links = array();
1423 foreach ($tree as $item) {
1424 if (!$item['link']['hidden']) {
1425 $class = '';
1426 $l = $item['link']['localized_options'];
1427 $l['href'] = $item['link']['href'];
1428 $l['title'] = $item['link']['title'];
1429 if ($item['link']['in_active_trail']) {
1430 $class = ' active-trail';
1431 }
1432 // Keyed with the unique mlid to generate classes in theme_links().
1433 $links['menu-' . $item['link']['mlid'] . $class] = $l;
1434 }
1435 }
1436 return $links;
1437 }
1438
1439 /**
1440 * Collects the local tasks (tabs) for a given level.
1441 *
1442 * @param $level
1443 * The level of tasks you ask for. Primary tasks are 0, secondary are 1.
1444 * @param $return_root
1445 * Whether to return the root path for the current page.
1446 * @return
1447 * Themed output corresponding to the tabs of the requested level, or
1448 * router path if $return_root == TRUE. This router path corresponds to
1449 * a parent tab, if the current page is a default local task.
1450 */
1451 function menu_local_tasks($level = 0, $return_root = FALSE) {
1452 $tabs = &drupal_static(__FUNCTION__);
1453 $root_path = &drupal_static(__FUNCTION__ . ':root_path');
1454
1455 if (!isset($tabs)) {
1456 $tabs = array();
1457
1458 $router_item = menu_get_item();
1459 if (!$router_item || !$router_item['access']) {
1460 return '';
1461 }
1462 // Get all tabs and the root page.
1463 $result = db_select('menu_router', NULL, array('fetch' => PDO::FETCH_ASSOC))
1464 ->fields('menu_router')
1465 ->condition('tab_root', $router_item['tab_root'])
1466 ->orderBy('weight')
1467 ->orderBy('title')
1468 ->execute();
1469 $map = arg();
1470 $children = array();
1471 $tasks = array();
1472 $root_path = $router_item['path'];
1473
1474 foreach ($result as $item) {
1475 _menu_translate($item, $map, TRUE);
1476 if ($item['tab_parent']) {
1477 // All tabs, but not the root page.
1478 $children[$item['tab_parent']][$item['path']] = $item;
1479 }
1480 // Store the translated item for later use.
1481 $tasks[$item['path']] = $item;
1482 }
1483
1484 // Find all tabs below the current path.
1485 $path = $router_item['path'];
1486 // Tab parenting may skip levels, so the number of parts in the path may not
1487 // equal the depth. Thus we use the $depth counter (offset by 1000 for ksort).
1488 $depth = 1001;
1489 while (isset($children[$path])) {
1490 $tabs_current = '';
1491 $next_path = '';
1492 $count = 0;
1493 foreach ($children[$path] as $item) {
1494 if ($item['access']) {
1495 $count++;
1496 // The default task is always active.
1497 if ($item['type'] == MENU_DEFAULT_LOCAL_TASK) {
1498 // Find the first parent which is not a default local task.
1499 for ($p = $item['tab_parent']; $tasks[$p]['type'] == MENU_DEFAULT_LOCAL_TASK; $p = $tasks[$p]['tab_parent']);
1500 $link = theme('menu_item_link', array('href' => $tasks[$p]['href']) + $item);
1501 $tabs_current .= theme('menu_local_task', $link, TRUE);
1502 $next_path = $item['path'];
1503 }
1504 else {
1505 $link = theme('menu_item_link', $item);
1506 $tabs_current .= theme('menu_local_task', $link);
1507 }
1508 }
1509 }
1510 $path = $next_path;
1511 $tabs[$depth]['count'] = $count;
1512 $tabs[$depth]['output'] = $tabs_current;
1513 $depth++;
1514 }
1515
1516 // Find all tabs at the same level or above the current one.
1517 $parent = $router_item['tab_parent'];
1518 $path = $router_item['path'];
1519 $current = $router_item;
1520 $depth = 1000;
1521 while (isset($children[$parent])) {
1522 $tabs_current = '';
1523 $next_path = '';
1524 $next_parent = '';
1525 $count = 0;
1526 foreach ($children[$parent] as $item) {
1527 if ($item['access']) {
1528 $count++;
1529 if ($item['type'] == MENU_DEFAULT_LOCAL_TASK) {
1530 // Find the first parent which is not a default local task.
1531 for ($p = $item['tab_parent']; $tasks[$p]['type'] == MENU_DEFAULT_LOCAL_TASK; $p = $tasks[$p]['tab_parent']);
1532 $link = theme('menu_item_link', array('href' => $tasks[$p]['href']) + $item);
1533 if ($item['path'] == $router_item['path']) {
1534 $root_path = $tasks[$p]['path'];
1535 }
1536 }
1537 else {
1538 $link = theme('menu_item_link', $item);
1539 }
1540 // We check for the active tab.
1541 if ($item['path'] == $path) {
1542 $tabs_current .= theme('menu_local_task', $link, TRUE);
1543 $next_path = $item['tab_parent'];
1544 if (isset($tasks[$next_path])) {
1545 $next_parent = $tasks[$next_path]['tab_parent'];
1546 }
1547 }
1548 else {
1549 $tabs_current .= theme('menu_local_task', $link);
1550 }
1551 }
1552 }
1553 $path = $next_path;
1554 $parent = $next_parent;
1555 $tabs[$depth]['count'] = $count;
1556 $tabs[$depth]['output'] = $tabs_current;
1557 $depth--;
1558 }
1559 // Sort by depth.
1560 ksort($tabs);
1561 // Remove the depth, we are interested only in their relative placement.
1562 $tabs = array_values($tabs);
1563 }
1564
1565 if ($return_root) {
1566 return $root_path;
1567 }
1568 else {
1569 // We do not display single tabs.
1570 return (isset($tabs[$level]) && $tabs[$level]['count'] > 1) ? $tabs[$level]['output'] : '';
1571 }
1572 }
1573
1574 /**
1575 * Returns the rendered local tasks at the top level.
1576 */
1577 function menu_primary_local_tasks() {
1578 return menu_local_tasks(0);
1579 }
1580
1581 /**
1582 * Returns the rendered local tasks at the second level.
1583 */
1584 function menu_secondary_local_tasks() {
1585 return menu_local_tasks(1);
1586 }
1587
1588 /**
1589 * Returns the router path, or the path of the parent tab of a default local task.
1590 */
1591 function menu_tab_root_path() {
1592 return menu_local_tasks(0, TRUE);
1593 }
1594
1595 /**
1596 * Returns the rendered local tasks. The default implementation renders them as tabs.
1597 *
1598 * @ingroup themeable
1599 */
1600 function theme_menu_local_tasks() {
1601 $output = '';
1602
1603 if ($primary = menu_primary_local_tasks()) {
1604 $output .= "<ul class=\"tabs primary\">\n" . $primary . "</ul>\n";
1605 }
1606 if ($secondary = menu_secondary_local_tasks()) {
1607 $output .= "<ul class=\"tabs secondary\">\n" . $secondary . "</ul>\n";
1608 }
1609
1610 return $output;
1611 }
1612
1613 /**
1614 * Set (or get) the active menu for the current page - determines the active trail.
1615 */
1616 function menu_set_active_menu_names($menu_names = NULL) {
1617 $active = &drupal_static(__FUNCTION__);
1618
1619 if (isset($menu_names) && is_array($menu_names)) {
1620 $active = $menu_names;
1621 }
1622 elseif (!isset($active)) {
1623 $active = variable_get('menu_default_active_menus', array('management', 'navigation', 'user-menu', 'main-menu'));
1624 }
1625 return $active;
1626 }
1627
1628 /**
1629 * Get the active menu for the current page - determines the active trail.
1630 */
1631 function menu_get_active_menu_names() {
1632 return menu_set_active_menu_names();
1633 }
1634
1635 /**
1636 * Set the active path, which determines which page is loaded.
1637 *
1638 * @param $path
1639 * A Drupal path - not a path alias.
1640 *
1641 * Note that this may not have the desired effect unless invoked very early
1642 * in the page load, such as during hook_boot, or unless you call
1643 * menu_execute_active_handler() to generate your page output.
1644 */
1645 function menu_set_active_item($path) {
1646 $_GET['q'] = $path;
1647 }
1648
1649 /**
1650 * Set (or get) the active trail for the current page - the path to root in the menu tree.
1651 */
1652 function menu_set_active_trail($new_trail = NULL) {
1653 $trail = &drupal_static(__FUNCTION__);
1654
1655 if (isset($new_trail)) {
1656 $trail = $new_trail;
1657 }
1658 elseif (!isset($trail)) {
1659 $trail = array();
1660 $trail[] = array('title' => t('Home'), 'href' => '<front>', 'localized_options' => array(), 'type' => 0);
1661 $item = menu_get_item();
1662
1663 // Check whether the current item is a local task (displayed as a tab).
1664 if ($item['tab_parent']) {
1665 // The title of a local task is used for the tab, never the page title.
1666 // Thus, replace it with the item corresponding to the root path to get
1667 // the relevant href and title. For example, the menu item corresponding
1668 // to 'admin' is used when on the 'By module' tab at 'admin/by-module'.
1669 $parts = explode('/', $item['tab_root']);
1670 $args = arg();
1671 // Replace wildcards in the root path using the current path.
1672 foreach ($parts as $index => $part) {
1673 if ($part == '%') {
1674 $parts[$index] = $args[$index];
1675 }
1676 }
1677 // Retrieve the menu item using the root path after wildcard replacement.
1678 $root_item = menu_get_item(implode('/', $parts));
1679 if ($root_item && $root_item['access']) {
1680 $item = $root_item;
1681 }
1682 }
1683 $menu_names = menu_get_active_menu_names();
1684 $curr = FALSE;
1685 // Determine if the current page is a link in any of the active menus.
1686 if ($menu_names) {
1687 $query = db_select('menu_links', 'ml');
1688 $query->fields('ml', array('menu_name'));
1689 $query->condition('ml.link_path', $item['href']);
1690 $query->condition('ml.menu_name', $menu_names, 'IN');
1691 $result = $query->execute();
1692 $found = array();
1693 foreach ($result as $menu) {
1694 $found[] = $menu->menu_name;
1695 }
1696 // The $menu_names array is ordered, so take the first one that matches.
1697 $name = current(array_intersect($menu_names, $found));
1698 if ($name !== FALSE) {
1699 $tree = menu_tree_page_data($name);
1700 list($key, $curr) = each($tree);
1701 }
1702 }
1703
1704 while ($curr) {
1705 // Terminate the loop when we find the current path in the active trail.
1706 if ($curr['link']['href'] == $item['href']) {
1707 $trail[] = $curr['link'];
1708 $curr = FALSE;
1709 }
1710 else {
1711 // Add the link if it's in the active trail, then move to the link below.
1712 if ($curr['link']['in_active_trail']) {
1713 $trail[] = $curr['link'];
1714 $tree = $curr['below'] ? $curr['below'] : array();
1715 }
1716 list($key, $curr) = each($tree);
1717 }
1718 }
1719 // Make sure the current page is in the trail (needed for the page title),
1720 // but exclude tabs and the front page.
1721 $last = count($trail) - 1;
1722 if ($trail[$last]['href'] != $item['href'] && !(bool)($item['type'] & MENU_IS_LOCAL_TASK) && !drupal_is_front_page()) {
1723 $trail[] = $item;
1724 }
1725 }
1726 return $trail;
1727 }
1728
1729 /**
1730 * Get the active trail for the current page - the path to root in the menu tree.
1731 */
1732 function menu_get_active_trail() {
1733 return menu_set_active_trail();
1734 }
1735
1736 /**
1737 * Get the breadcrumb for the current page, as determined by the active trail.
1738 */
1739 function menu_get_active_breadcrumb() {
1740 $breadcrumb = array();
1741
1742 // No breadcrumb for the front page.
1743 if (drupal_is_front_page()) {
1744 return $breadcrumb;
1745 }
1746
1747 $item = menu_get_item();
1748 if ($item && $item['access']) {
1749 $active_trail = menu_get_active_trail();
1750
1751 foreach ($active_trail as $parent) {
1752 $breadcrumb[] = l($parent['title'], $parent['href'], $parent['localized_options']);
1753 }
1754 $end = end($active_trail);
1755
1756 // Don't show a link to the current page in the breadcrumb trail.
1757 if ($item['href'] == $end['href'] || ($item['type'] == MENU_DEFAULT_LOCAL_TASK && $end['href'] != '<front>')) {
1758 array_pop($breadcrumb);
1759 }
1760 }
1761 return $breadcrumb;
1762 }
1763
1764 /**
1765 * Get the title of the current page, as determined by the active trail.
1766 */
1767 function menu_get_active_title() {
1768 $active_trail = menu_get_active_trail();
1769
1770 foreach (array_reverse($active_trail) as $item) {
1771 if (!(bool)($item['type'] & MENU_IS_LOCAL_TASK)) {
1772 return $item['title'];
1773 }
1774 }
1775 }
1776
1777 /**
1778 * Get a menu link by its mlid, access checked and link translated for rendering.
1779 *
1780 * This function should never be called from within node_load() or any other
1781 * function used as a menu object load function since an infinite recursion may
1782 * occur.
1783 *
1784 * @param $mlid
1785 * The mlid of the menu item.
1786 * @return
1787 * A menu link, with $item['access'] filled and link translated for
1788 * rendering.
1789 */
1790 function menu_link_load($mlid) {
1791 if (is_numeric($mlid)) {
1792 $query = db_select('menu_links', 'ml');
1793 $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
1794 $query->fields('ml');
1795 $query->fields('m');
1796 $query->condition('ml.mlid', $mlid);
1797 if ($item = $query->execute()->fetchAssoc()) {
1798 _menu_link_translate($item);
1799 return $item;
1800 }
1801 }
1802 return FALSE;
1803 }
1804
1805 /**
1806 * Clears the cached cached data for a single named menu.
1807 */
1808 function menu_cache_clear($menu_name = 'navigation') {
1809 $cache_cleared = &drupal_static(__FUNCTION__, array());
1810
1811 if (empty($cache_cleared[$menu_name])) {
1812 cache_clear_all('links:' . $menu_name . ':', 'cache_menu', TRUE);
1813 $cache_cleared[$menu_name] = 1;
1814 }
1815 elseif ($cache_cleared[$menu_name] == 1) {
1816 register_shutdown_function('cache_clear_all', 'links:' . $menu_name . ':', 'cache_menu', TRUE);
1817 $cache_cleared[$menu_name] = 2;
1818 }
1819 }
1820
1821 /**
1822 * Clears all cached menu data. This should be called any time broad changes
1823 * might have been made to the router items or menu links.
1824 */
1825 function menu_cache_clear_all() {
1826 cache_clear_all('*', 'cache_menu', TRUE);
1827 }
1828
1829 /**
1830 * (Re)populate the database tables used by various menu functions.
1831 *
1832 * This function will clear and populate the {menu_router} table, add entries
1833 * to {menu_links} for new router items, then remove stale items from
1834 * {menu_links}. If called from update.php or install.php, it will also
1835 * schedule a call to itself on the first real page load from
1836 * menu_execute_active_handler(), because the maintenance page environment
1837 * is different and leaves stale data in the menu tables.
1838 */
1839 function menu_rebuild() {
1840 variable_del('menu_rebuild_needed');
1841 list($menu, $masks) = menu_router_build();
1842 _menu_router_save($menu, $masks);
1843 _menu_navigation_links_rebuild($menu);
1844 menu_cache_clear_all();
1845 // Clear the page and block caches.
1846 _menu_clear_page_cache();
1847 if (defined('MAINTENANCE_MODE')) {
1848 variable_set('menu_rebuild_needed', TRUE);
1849 }
1850 }
1851
1852 /**
1853 * Collect and alter the menu definitions.
1854 */
1855 function menu_router_build() {
1856 // We need to manually call each module so that we can know which module
1857 // a given item came from.
1858 $callbacks = array();
1859 foreach (module_implements('menu') as $module) {
1860 $router_items = call_user_func($module . '_menu');
1861 if (isset($router_items) && is_array($router_items)) {
1862 foreach (array_keys($router_items) as $path) {
1863 $router_items[$path]['module'] = $module;
1864 }
1865 $callbacks = array_merge($callbacks, $router_items);
1866 }
1867 }
1868 // Alter the menu as defined in modules, keys are like user/%user.
1869 drupal_alter('menu', $callbacks);
1870 list($menu, $masks) = _menu_router_build($callbacks);
1871 _menu_router_cache($menu);
1872
1873 return array($menu, $masks);
1874 }
1875
1876 /**
1877 * Helper function to store the menu router if we have it in memory.
1878 */
1879 function _menu_router_cache($new_menu = NULL) {
1880 $menu = &drupal_static(__FUNCTION__);
1881
1882 if (isset($new_menu)) {
1883 $menu = $new_menu;
1884 }
1885 return $menu;
1886 }
1887
1888 /**
1889 * Get the menu router.
1890 */
1891 function menu_get_router() {
1892 // Check first if we have it in memory already.
1893 $menu = _menu_router_cache();
1894 if (empty($menu)) {
1895 list($menu, $masks) = menu_router_build();
1896 }
1897 return $menu;
1898 }
1899
1900 /**
1901 * Builds a link from a router item.
1902 */
1903 function _menu_link_build($item) {
1904 if ($item['type'] == MENU_CALLBACK) {
1905 $item['hidden'] = -1;
1906 }
1907 elseif ($item['type'] == MENU_SUGGESTED_ITEM) {
1908 $item['hidden'] = 1;
1909 }
1910 // Note, we set this as 'system', so that we can be sure to distinguish all
1911 // the menu links generated automatically from entries in {menu_router}.
1912 $item['module'] = 'system';
1913 $item += array(
1914 'menu_name' => 'navigation',
1915 'link_title' => $item['title'],
1916 'link_path' => $item['path'],
1917 'hidden' => 0,
1918 'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])),
1919 );
1920 return $item;
1921 }
1922
1923 /**
1924 * Helper function to build menu links for the items in the menu router.
1925 */
1926 function _menu_navigation_links_rebuild($menu) {
1927 // Add normal and suggested items as links.
1928 $menu_links = array();
1929 foreach ($menu as $path => $item) {
1930 if ($item['_visible']) {
1931 $menu_links[$path] = $item;
1932 $sort[$path] = $item['_number_parts'];
1933 }
1934 }
1935 if ($menu_links) {
1936 // Make sure no child comes before its parent.
1937 array_multisort($sort, SORT_NUMERIC, $menu_links);
1938
1939 foreach ($menu_links as $item) {
1940 $existing_item = db_select('menu_links')
1941 ->fields('menu_links', array(
1942 'mlid',
1943 'menu_name',
1944 'plid',
1945 'customized',
1946 'has_children',
1947 'updated',
1948 ))
1949 ->condition('link_path', $item['path'])
1950 ->condition('module', 'system')
1951 ->execute()->fetchAssoc();
1952 if ($existing_item) {
1953 $item['mlid'] = $existing_item['mlid'];
1954 // A change in hook_menu may move the link to a different menu
1955 if (empty($item['menu_name']) || ($item['menu_name'] == $existing_item['menu_name'])) {
1956 $item['menu_name'] = $existing_item['menu_name'];
1957 $item['plid'] = $existing_item['plid'];
1958 }
1959 else {
1960 // If it moved, put it at the top level in the new menu.
1961 $item['plid'] = 0;
1962 }
1963 $item['has_children'] = $existing_item['has_children'];
1964 $item['updated'] = $existing_item['updated'];
1965 }
1966 if (!$existing_item || !$existing_item['customized']) {
1967 $item = _menu_link_build($item);
1968 menu_link_save($item);
1969 }
1970 }
1971 }
1972 $paths = array_keys($menu);
1973 // Updated and customized items whose router paths are gone need new ones.
1974 $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC))
1975 ->fields('menu_links', array(
1976 'link_path',
1977 'mlid',
1978 'router_path',
1979 'updated',
1980 ))
1981 ->condition(db_or()
1982 ->condition('updated', 1)
1983 ->condition(db_and()
1984 ->condition('router_path', $paths, 'NOT IN')
1985 ->condition('external', 0)
1986 ->condition('customized', 1)
1987 )
1988 )
1989 ->execute();
1990 foreach ($result as $item) {
1991 $router_path = _menu_find_router_path($item['link_path']);
1992 if (!empty($router_path) && ($router_path != $item['router_path'] || $item['updated'])) {
1993 // If the router path and the link path matches, it's surely a working
1994 // item, so we clear the updated flag.
1995 $updated = $item['updated'] && $router_path != $item['link_path'];
1996 db_update('menu_links')
1997 ->fields(array(
1998 'router_path' => $router_path,
1999 'updated' => (int) $updated,
2000 ))
2001 ->condition('mlid', $item['mlid'])
2002 ->execute();
2003 }
2004 }
2005 // Find any item whose router path does not exist any more.
2006 $result = db_select('menu_links')
2007 ->fields('menu_links')
2008 ->condition('router_path', $paths, 'NOT IN')
2009 ->condition('external', 0)
2010 ->condition('updated', 0)
2011 ->condition('customized', 0)
2012 ->orderBy('depth', 'DESC')
2013 ->execute();
2014 // Remove all such items. Starting from those with the greatest depth will
2015 // minimize the amount of re-parenting done by menu_link_delete().
2016 foreach ($result as $item) {
2017 _menu_delete_item($item, TRUE);
2018 }
2019 }
2020
2021 /**
2022 * Delete one or several menu links.
2023 *
2024 * @param $mlid
2025 * A valid menu link mlid or NULL. If NULL, $path is used.
2026 * @param $path
2027 * The path to the menu items to be deleted. $mlid must be NULL.
2028 */
2029 function menu_link_delete($mlid, $path = NULL) {
2030 if (isset($mlid)) {
2031 _menu_delete_item(db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc());
2032 }
2033 else {
2034 $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(':link_path' => $path));
2035 foreach ($result as $link) {
2036 _menu_delete_item($link);
2037 }
2038 }
2039 }
2040
2041 /**
2042 * Helper function for menu_link_delete; deletes a single menu link.
2043 *
2044 * @param $item
2045 * Item to be deleted.
2046 * @param $force
2047 * Forces deletion. Internal use only, setting to TRUE is discouraged.
2048 */
2049 function _menu_delete_item($item, $force = FALSE) {
2050 $item = is_object($item) ? get_object_vars($item) : $item;
2051 if ($item && ($item['module'] != 'system' || $item['updated'] || $force)) {
2052 // Children get re-attached to the item's parent.
2053 if ($item['has_children']) {
2054 $result = db_query("SELECT mlid FROM {menu_links} WHERE plid = :plid", array(':plid' => $item['mlid']));
2055 foreach ($result as $m) {
2056 $child = menu_link_load($m->mlid);
2057 $child['plid'] = $item['plid'];
2058 menu_link_save($child);
2059 }
2060 }
2061 db_delete('menu_links')->condition('mlid', $item['mlid'])->execute();
2062
2063 // Update the has_children status of the parent.
2064 _menu_update_parental_status($item);
2065 menu_cache_clear($item['menu_name']);
2066 _menu_clear_page_cache();
2067 }
2068 }
2069
2070 /**
2071 * Save a menu link.
2072 *
2073 * @param $item
2074 * An array representing a menu link item. The only mandatory keys are
2075 * link_path and link_title. Possible keys are:
2076 * - menu_name default is navigation
2077 * - weight default is 0
2078 * - expanded whether the item is expanded.
2079 * - options An array of options, @see l for more.
2080 * - mlid Set to an existing value, or 0 or NULL to insert a new link.
2081 * - plid The mlid of the parent.
2082 * - router_path The path of the relevant router item.
2083 * @return
2084 * The mlid of the saved menu link, or FALSE if the menu link could not be
2085 * saved.
2086 */
2087 function menu_link_save(&$item) {
2088
2089 drupal_alter('menu_link', $item);
2090
2091 // This is the easiest way to handle the unique internal path '<front>',
2092 // since a path marked as external does not need to match a router path.
2093 $item['external'] = (menu_path_is_external($item['link_path']) || $item['link_path'] == '<front>') ? 1 : 0;
2094 // Load defaults.
2095 $item += array(
2096 'menu_name' => 'navigation',
2097 'weight' => 0,
2098 'link_title' => '',
2099 'hidden' => 0,
2100 'has_children' => 0,
2101 'expanded' => 0,
2102 'options' => array(),
2103 'module' => 'menu',
2104 'customized' => 0,
2105 'updated' => 0,
2106 );
2107 $existing_item = FALSE;
2108 if (isset($item['mlid'])) {
2109 if ($existing_item = db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $item['mlid']))->fetchAssoc()) {
2110 $existing_item['options'] = unserialize($existing_item['options']);
2111 }
2112 }
2113
2114 if (isset($item['plid'])) {
2115 if ($item['plid']) {
2116 $parent = db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $item['plid']))->fetchAssoc();
2117 }
2118 else {
2119 // Don't bother with the query - mlid can never equal zero..
2120 $parent = FALSE;
2121 }
2122 }
2123 else {
2124 $query = db_select('menu_links');
2125 // Only links derived from router items should have module == 'system', and
2126 // we want to find the parent even if it's in a different menu.
2127 if ($item['module'] == 'system') {
2128 $query->condition('module', 'system');
2129 }
2130 else {
2131 // If not derived from a router item, we respect the specified menu name.
2132 $query->condition('menu_name', $item['menu_name']);
2133 }
2134
2135 // Find the parent - it must be unique.
2136 $parent_path = $item['link_path'];
2137 do {
2138 $parent = FALSE;
2139 $parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
2140 $new_query = clone $query;
2141 $new_query->condition('link_path', $parent_path);
2142 // Only valid if we get a unique result.
2143 if ($new_query->countQuery()->execute()->fetchField() == 1) {
2144 $parent = $new_query->fields('menu_links')->execute()->fetchAssoc();
2145 }
2146 } while ($parent === FALSE && $parent_path);
2147 }
2148 if ($parent !== FALSE) {
2149 $item['menu_name'] = $parent['menu_name'];
2150 }
2151 $menu_name = $item['menu_name'];
2152 // Menu callbacks need to be in the links table for breadcrumbs, but can't
2153 // be parents if they are generated directly from a router item.
2154 if (empty($parent['mlid']) || $parent['hidden'] < 0) {
2155 $item['plid'] = 0;
2156 }
2157 else {
2158 $item['plid'] = $parent['mlid'];
2159 }
2160
2161 if (!$existing_item) {
2162 $item['mlid'] = db_insert('menu_links')
2163 ->fields(array(
2164 'menu_name' => $item['menu_name'],
2165 'plid' => $item['plid'],
2166 'link_path' => $item['link_path'],
2167 'hidden' => $item['hidden'],
2168 'external' => $item['external'],
2169 'has_children' => $item['has_children'],
2170 'expanded' => $item['expanded'],
2171 'weight' => $item['weight'],
2172 'module' => $item['module'],
2173 'link_title' => $item['link_title'],
2174 'options' => serialize($item['options']),
2175 'customized' => $item['customized'],
2176 'updated' => $item['updated'],
2177 ))
2178 ->execute();
2179 }
2180
2181 if (!$item['plid']) {
2182 $item['p1'] = $item['mlid'];
2183 for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
2184 $item["p$i"] = 0;
2185 }
2186 $item['depth'] = 1;
2187 }
2188 else {
2189 // Cannot add beyond the maximum depth.
2190 if ($item['has_children'] && $existing_item) {
2191 $limit = MENU_MAX_DEPTH - menu_link_children_relative_depth($existing_item) - 1;
2192 }
2193 else {
2194 $limit = MENU_MAX_DEPTH - 1;
2195 }
2196 if ($parent['depth'] > $limit) {
2197 return FALSE;
2198 }
2199 $item['depth'] = $parent['depth'] + 1;
2200 _menu_link_parents_set($item, $parent);
2201 }
2202 // Need to check both plid and menu_name, since plid can be 0 in any menu.
2203 if ($existing_item && ($item['plid'] != $existing_item['plid'] || $menu_name != $existing_item['menu_name'])) {
2204 _menu_link_move_children($item, $existing_item);
2205 }
2206 // Find the router_path.
2207 if (empty($item['router_path']) || !$existing_item || ($existing_item['link_path'] != $item['link_path'])) {
2208 if ($item['external']) {
2209 $item['router_path'] = '';
2210 }
2211 else {
2212 // Find the router path which will serve this path.
2213 $item['parts'] = explode('/', $item['link_path'], MENU_MAX_PARTS);
2214 $item['router_path'] = _menu_find_router_path($item['link_path']);
2215 }
2216 }
2217 // If every value in $existing_item is the same in the $item, there is no
2218 // reason to run the update queries or clear the caches. We use
2219 // array_intersect_assoc() with the $item as the first parameter because
2220 // $item may have additional keys left over from building a router entry.
2221 // The intersect removes the extra keys, allowing a meaningful comparison.
2222 if (!$existing_item || (array_intersect_assoc($item, $existing_item)) != $existing_item) {
2223 db_update('menu_links')
2224 ->fields(array(
2225 'menu_name' => $item['menu_name'],
2226 'plid' => $item['plid'],
2227 'link_path' => $item['link_path'],
2228 'router_path' => $item['router_path'],
2229 'hidden' => $item['hidden'],
2230 'external' => $item['external'],
2231 'has_children' => $item['has_children'],
2232 'expanded' => $item['expanded'],
2233 'weight' => $item['weight'],
2234 'depth' => $item['depth'],
2235 'p1' => $item['p1'],
2236 'p2' => $item['p2'],
2237 'p3' => $item['p3'],
2238 'p4' => $item['p4'],
2239 'p5' => $item['p5'],
2240 'p6' => $item['p6'],
2241 'p7' => $item['p7'],
2242 'p8' => $item['p8'],
2243 'p9' => $item['p9'],
2244 'module' => $item['module'],
2245 'link_title' => $item['link_title'],
2246 'options' => serialize($item['options']),
2247 'customized' => $item['customized'],
2248 ))
2249 ->condition('mlid', $item['mlid'])
2250 ->execute();
2251 // Check the has_children status of the parent.
2252 _menu_update_parental_status($item);
2253 menu_cache_clear($menu_name);
2254 if ($existing_item && $menu_name != $existing_item['menu_name']) {
2255 menu_cache_clear($existing_item['menu_name']);
2256 }
2257
2258 _menu_clear_page_cache();
2259 }
2260 return $item['mlid'];
2261 }
2262
2263 /**
2264 * Helper function to clear the page and block caches at most twice per page load.
2265 */
2266 function _menu_clear_page_cache() {
2267 $cache_cleared = &drupal_static(__FUNCTION__, 0);
2268
2269 // Clear the page and block caches, but at most twice, including at
2270 // the end of the page load when there are multiple links saved or deleted.
2271 if ($cache_cleared == 0) {
2272 cache_clear_all();
2273 // Keep track of which menus have expanded items.
2274 _menu_set_expanded_menus();
2275 $cache_cleared = 1;
2276 }
2277 elseif ($cache_cleared == 1) {
2278 register_shutdown_function('cache_clear_all');
2279 // Keep track of which menus have expanded items.
2280 register_shutdown_function('_menu_set_expanded_menus');
2281 $cache_cleared = 2;
2282 }
2283 }
2284
2285 /**
2286 * Helper function to update a list of menus with expanded items
2287 */
2288 function _menu_set_expanded_menus() {
2289 $names = db_query("SELECT menu_name FROM {menu_links} WHERE expanded <> 0 GROUP BY menu_name")->fetchCol();
2290 variable_set('menu_expanded', $names);
2291 }
2292
2293 /**
2294 * Find the router path which will serve this path.
2295 *
2296 * @param $link_path
2297 * The path for we are looking up its router path.
2298 * @return
2299 * A path from $menu keys or empty if $link_path points to a nonexisting
2300 * place.
2301 */
2302 function _menu_find_router_path($link_path) {
2303 // $menu will only have data during a menu rebuild.
2304 $menu = _menu_router_cache();
2305
2306 $router_path = $link_path;
2307 $parts = explode('/', $link_path, MENU_MAX_PARTS);
2308 $ancestors = menu_get_ancestors($parts);
2309
2310 if (empty($menu)) {
2311 // Not during a menu rebuild, so look up in the database.
2312 $router_path = (string) db_select('menu_router')
2313 ->fields('menu_router', array('path'))
2314 ->condition('path', $ancestors, 'IN')
2315 ->orderBy('fit', 'DESC')
2316 ->range(0, 1)
2317 ->execute()->fetchField();
2318 }
2319 elseif (!isset($menu[$router_path])) {
2320 // Add an empty router path as a fallback.
2321 $ancestors[] = '';
2322 foreach ($ancestors as $key => $router_path) {
2323 if (isset($menu[$router_path])) {
2324 // Exit the loop leaving $router_path as the first match.
2325 break;
2326 }
2327 }
2328 // If we did not find the path, $router_path will be the empty string
2329 // at the end of $ancestors.
2330 }
2331 return $router_path;
2332 }
2333
2334 /**
2335 * Insert, update or delete an uncustomized menu link related to a module.
2336 *
2337 * @param $module
2338 * The name of the module.
2339 * @param $op
2340 * Operation to perform: insert, update or delete.
2341 * @param $link_path
2342 * The path this link points to.
2343 * @param $link_title
2344 * Title of the link to insert or new title to update the link to.
2345 * Unused for delete.
2346 * @return
2347 * The insert op returns the mlid of the new item. Others op return NULL.
2348 */
2349 function menu_link_maintain($module, $op, $link_path, $link_title) {
2350 switch ($op) {
2351 case 'insert':
2352 $menu_link = array(
2353 'link_title' => $link_title,
2354 'link_path' => $link_path,
2355 'module' => $module,
2356 );
2357 return menu_link_save($menu_link);
2358 break;
2359 case 'update':
2360 db_update('menu_links')
2361 ->fields(array('link_title' => $link_title))
2362 ->condition('link_path', $link_path)
2363 ->condition('customized', 0)
2364 ->condition('module', $module)
2365 ->execute();
2366 $result = db_select('menu_links')
2367 ->fields('menu_links', array('menu_name'))
2368 ->condition('link_path', $link_path)
2369 ->condition('customized', 0)
2370 ->condition('module', $module)
2371 ->groupBy('menu_name')
2372 ->execute()->fetchCol();
2373 foreach ($result as $menu_name) {
2374 menu_cache_clear($menu_name);
2375 }
2376 break;
2377 case 'delete':
2378 menu_link_delete(NULL, $link_path);
2379 break;
2380 }
2381 }
2382
2383 /**
2384 * Find the depth of an item's children relative to its depth.
2385 *
2386 * For example, if the item has a depth of 2, and the maximum of any child in
2387 * the menu link tree is 5, the relative depth is 3.
2388 *
2389 * @param $item
2390 * An array representing a menu link item.
2391 * @return
2392 * The relative depth, or zero.
2393 *
2394 */
2395 function menu_link_children_relative_depth($item) {
2396 $query = db_select('menu_links');
2397 $query->addField('menu_links', 'depth');
2398 $query->condition('menu_name', $item['menu_name']);
2399 $query->orderBy('depth', 'DESC');
2400 $query->range(0, 1);
2401
2402 $i = 1;
2403 $p = 'p1';
2404 while ($i <= MENU_MAX_DEPTH && $item[$p]) {
2405 $query->condition($p, $item[$p]);
2406 $p = 'p' . ++$i;
2407 }
2408
2409 $max_depth = $query->execute()->fetchField();
2410
2411 return ($max_depth > $item['depth']) ? $max_depth - $item['depth'] : 0;
2412 }
2413
2414 /**
2415 * Update the children of a menu link that's being moved.
2416 *
2417 * The menu name, parents (p1 - p6), and depth are updated for all children of
2418 * the link, and the has_children status of the previous parent is updated.
2419 */
2420 function _menu_link_move_children($item, $existing_item) {
2421 $query = db_update('menu_links');
2422
2423 $query->fields(array('menu_name' => $item['menu_name']));
2424
2425 $p = 'p1';
2426 for ($i = 1; $i <= $item['depth']; $p = 'p' . ++$i) {
2427 $query->fields(array($p => $item[$p]));
2428 }
2429 $j = $existing_item['depth'] + 1;
2430 while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) {
2431 $query->expression('p' . $i++, 'p' . $j++);
2432 }
2433 while ($i <= MENU_MAX_DEPTH) {
2434 $query->fields(array('p' . $i++ => 0));
2435 }
2436
2437 $shift = $item['depth'] - $existing_item['depth'];
2438 if ($shift < 0) {
2439 $query->expression('depth', 'depth - :depth', array(':depth' => -$shift));
2440 }
2441 elseif ($shift > 0) {
2442 $query->expression('depth', 'depth + :depth', array(':depth' => $shift));
2443 }
2444
2445 $query->condition('menu_name', $existing_item['menu_name']);
2446 $p = 'p1';
2447 for ($i = 1; $i <= MENU_MAX_DEPTH && $existing_item[$p]; $p = 'p' . ++$i) {
2448 $query->condition($p, $existing_item[$p]);
2449 }
2450
2451 $query->execute();
2452
2453 // Check the has_children status of the parent, while excluding this item.
2454 _menu_update_parental_status($existing_item, TRUE);
2455 }
2456
2457 /**
2458 * Check and update the has_children status for the parent of a link.
2459 */
2460 function _menu_update_parental_status($item, $exclude = FALSE) {
2461 // If plid == 0, there is nothing to update.
2462 if ($item['plid']) {
2463 // Check if at least one visible child exists in the table.
2464 $query = db_select('menu_links');
2465 $query->addField('menu_links', 'mlid');
2466 $query->condition('menu_name', $item['menu_name']);
2467 $query->condition('hidden', 0);
2468 $query->condition('plid', $item['plid']);
2469 $query->range(0, 1);
2470 if ($exclude) {
2471 $query->condition('mlid', $item['mlid'], '<>');
2472 }
2473 $parent_has_children = ((bool) $query->execute()->fetchField()) ? 1 : 0;
2474 db_update('menu_links')
2475 ->fields(array('has_children' => $parent_has_children))
2476 ->condition('mlid', $item['plid'])
2477 ->execute();
2478 }
2479 }
2480
2481 /**
2482 * Helper function that sets the p1..p9 values for a menu link being saved.
2483 */
2484 function _menu_link_parents_set(&$item, $parent) {
2485 $i = 1;
2486 while ($i < $item['depth']) {
2487 $p = 'p' . $i++;
2488 $item[$p] = $parent[$p];
2489 }
2490 $p = 'p' . $i++;
2491 // The parent (p1 - p9) corresponding to the depth always equals the mlid.
2492 $item[$p] = $item['mlid'];
2493 while ($i <= MENU_MAX_DEPTH) {
2494 $p = 'p' . $i++;
2495 $item[$p] = 0;
2496 }
2497 }
2498
2499 /**
2500 * Helper function to build the router table based on the data from hook_menu.
2501 */
2502 function _menu_router_build($callbacks) {
2503 // First pass: separate callbacks from paths, making paths ready for
2504 // matching. Calculate fitness, and fill some default values.
2505 $menu = array();
2506 $masks = array();
2507 foreach ($callbacks as $path => $item) {
2508 $load_functions = array();
2509 $to_arg_functions = array();
2510 $fit = 0;
2511 $move = FALSE;
2512
2513 $parts = explode('/', $path, MENU_MAX_PARTS);
2514 $number_parts = count($parts);
2515 // We store the highest index of parts here to save some work in the fit
2516 // calculation loop.
2517 $slashes = $number_parts - 1;
2518 // Extract load and to_arg functions.
2519 foreach ($parts as $k => $part) {
2520 $match = FALSE;
2521 // Look for wildcards in the form allowed to be used in PHP functions,
2522 // because we are using these to construct the load function names.
2523 // See http://php.net/manual/en/language.functions.php for reference.
2524 if (preg_match('/^%(|[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$/', $part, $matches)) {
2525 if (empty($matches[1])) {
2526 $match = TRUE;
2527 $load_functions[$k] = NULL;
2528 }
2529 else {
2530 if (drupal_function_exists($matches[1] . '_to_arg')) {
2531 $to_arg_functions[$k] = $matches[1] . '_to_arg';
2532 $load_functions[$k] = NULL;
2533 $match = TRUE;
2534 }
2535 if (drupal_function_exists($matches[1] . '_load')) {
2536 $function = $matches[1] . '_load';
2537 // Create an array of arguments that will be passed to the _load
2538 // function when this menu path is checked, if 'load arguments'
2539 // exists.
2540 $load_functions[$k] = isset($item['load arguments']) ? array($function => $item['load arguments']) : $function;
2541 $match = TRUE;
2542 }
2543 }
2544 }
2545 if ($match) {
2546 $parts[$k] = '%';
2547 }
2548 else {
2549 $fit |= 1 << ($slashes - $k);
2550 }
2551 }
2552 if ($fit) {
2553 $move = TRUE;
2554 }
2555 else {
2556 // If there is no %, it fits maximally.
2557 $fit = (1 << $number_parts) - 1;
2558 }
2559 $masks[$fit] = 1;
2560 $item['load_functions'] = empty($load_functions) ? '' : serialize($load_functions);
2561 $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions);
2562 $item += array(
2563 'title' => '',
2564 'weight' => 0,
2565 'type' => MENU_NORMAL_ITEM,
2566 '_number_parts' => $number_parts,
2567 '_parts' => $parts,
2568 '_fit' => $fit,
2569 );
2570 $item += array(
2571 '_visible' => (bool)($item['type'] & MENU_VISIBLE_IN_BREADCRUMB),
2572 '_tab' => (bool)($item['type'] & MENU_IS_LOCAL_TASK),
2573 );
2574 if ($move) {
2575 $new_path = implode('/', $item['_parts']);
2576 $menu[$new_path] = $item;
2577 $sort[$new_path] = $number_parts;
2578 }
2579 else {
2580 $menu[$path] = $item;
2581 $sort[$path] = $number_parts;
2582 }
2583 }
2584 array_multisort($sort, SORT_NUMERIC, $menu);
2585 // Apply inheritance rules.
2586 foreach ($menu as $path => $v) {
2587 $item = &$menu[$path];
2588 if (!$item['_tab']) {
2589 // Non-tab items.
2590 $item['tab_parent'] = '';
2591 $item['tab_root'] = $path;
2592 }
2593 for ($i = $item['_number_parts'] - 1; $i; $i--) {
2594 $parent_path = implode('/', array_slice($item['_parts'], 0, $i));
2595 if (isset($menu[$parent_path])) {
2596
2597 $parent = $menu[$parent_path];
2598
2599 if (!isset($item['tab_parent'])) {
2600 // Parent stores the parent of the path.
2601 $item['tab_parent'] = $parent_path;
2602 }
2603 if (!isset($item['tab_root']) && !$parent['_tab']) {
2604 $item['tab_root'] = $parent_path;
2605 }
2606 // If an access callback is not found for a default local task we use
2607 // the callback from the parent, since we expect them to be identical.
2608 // In all other cases, the access parameters must be specified.
2609 if (($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access callback']) && isset($parent['access callback'])) {
2610 $item['access callback'] = $parent['access callback'];
2611 if (!isset($item['access arguments']) && isset($parent['access arguments'])) {
2612 $item['access arguments'] = $parent['access arguments'];
2613 }
2614 }
2615 // Same for page callbacks.
2616 if (!isset($item['page callback']) && isset($parent['page callback'])) {
2617 $item['page callback'] = $parent['page callback'];
2618 if (!isset($item['page arguments']) && isset($parent['page arguments'])) {
2619 $item['page arguments'] = $parent['page arguments'];
2620 }
2621 }
2622 }
2623 }
2624 if (!isset($item['access callback']) && isset($item['access arguments'])) {
2625 // Default callback.
2626 $item['access callback'] = 'user_access';
2627 }
2628 if (!isset($item['access callback']) || empty($item['page callback'])) {
2629 $item['access callback'] = 0;
2630 }
2631 if (is_bool($item['access callback'])) {
2632 $item['access callback'] = intval($item['access callback']);
2633 }
2634
2635 $item += array(
2636 'access arguments' => array(),
2637 'access callback' => '',
2638 'page arguments' => array(),
2639 'page callback' => '',
2640 'block callback' => '',
2641 'title arguments' => array(),
2642 'title callback' => 't',
2643 'description' => '',
2644 'position' => '',
2645 'tab_parent' => '',
2646 'tab_root' => $path,
2647 'path' => $path,
2648 );
2649 }
2650
2651 // Sort the masks so they are in order of descending fit.
2652 $masks = array_keys($masks);
2653 rsort($masks);
2654
2655 return array($menu, $masks);
2656 }
2657
2658 /**
2659 * Helper function to save data from menu_router_build() to the router table.
2660 */
2661 function _menu_router_save($menu, $masks) {
2662 // Delete the existing router since we have some data to replace it.
2663 db_delete('menu_router')->execute();
2664
2665 // Prepare insert object.
2666 $insert = db_insert('menu_router')
2667 ->fields(array(
2668 'path',
2669 'load_functions',
2670 'to_arg_functions',
2671 'access_callback',
2672 'access_arguments',
2673 'page_callback',
2674 'page_arguments',
2675 'fit',
2676 'number_parts',
2677 'tab_parent',
2678 'tab_root',
2679 'title',
2680 'title_callback',
2681 'title_arguments',
2682 'type',
2683 'block_callback',
2684 'description',
2685 'position',
2686 'weight',
2687 ));
2688
2689 foreach ($menu as $path => $item) {
2690 // Fill in insert object values.
2691 $insert->values(array(
2692 'path' => $item['path'],
2693 'load_functions' => $item['load_functions'],
2694 'to_arg_functions' => $item['to_arg_functions'],
2695 'access_callback' => $item['access callback'],
2696 'access_arguments' => serialize($item['access arguments']),
2697 'page_callback' => $item['page callback'],
2698 'page_arguments' => serialize($item['page arguments']),
2699 'fit' => $item['_fit'],
2700 'number_parts' => $item['_number_parts'],
2701 'tab_parent' => $item['tab_parent'],
2702 'tab_root' => $item['tab_root'],
2703 'title' => $item['title'],
2704 'title_callback' => $item['title callback'],
2705 'title_arguments' => ($item['title arguments'] ? serialize($item['title arguments']) : ''),
2706 'type' => $item['type'],
2707 'block_callback' => $item['block callback'],
2708 'description' => $item['description'],
2709 'position' => $item['position'],
2710 'weight' => $item['weight'],
2711 ));
2712 }
2713 // Execute insert object.
2714 $insert->execute();
2715 // Store the masks.
2716 variable_set('menu_masks', $masks);
2717
2718 return $menu;
2719 }
2720
2721 /**
2722 * Returns TRUE if a path is external (e.g. http://example.com).
2723 */
2724 function menu_path_is_external($path) {
2725 $colonpos = strpos($path, ':');
2726 return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && filter_xss_bad_protocol($path, FALSE) == check_plain($path);
2727 }
2728
2729 /**
2730 * Checks whether the site is offline for maintenance.
2731 *
2732 * This function will log the current user out and redirect to front page
2733 * if the current user has no 'administer site configuration' permission.
2734 *
2735 * @return
2736 * FALSE if the site is not offline or its the login page or the user has
2737 * 'administer site configuration' permission.
2738 * TRUE for anonymous users not on the login page if the site is offline.
2739 */
2740 function _menu_site_is_offline() {
2741 // Check if site is set to maintenance mode.
2742 if (variable_get('site_offline', 0)) {
2743 // Check if the user has administration privileges.
2744 if (user_access('administer site configuration')) {
2745 // Ensure that the offline message is displayed only once [allowing for
2746 // page redirects], and specifically suppress its display on the site
2747 // maintenance page.
2748 if (drupal_get_normal_path($_GET['q']) != 'admin/config/development/maintenance') {
2749 drupal_set_message(t('Operating in maintenance mode. <a href="@url">Go online.</a>', array('@url' => url('admin/config/development/maintenance'))), 'status', FALSE);
2750 }
2751 }
2752 else {
2753 // Anonymous users get a FALSE at the login prompt, TRUE otherwise.
2754 if (user_is_anonymous()) {
2755 return $_GET['q'] != 'user' && $_GET['q'] != 'user/login';
2756 }
2757 // Logged in users are unprivileged here, so they are logged out.
2758 require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'user') . '/user.pages.inc';
2759 user_logout();
2760 }
2761 }
2762 return FALSE;
2763 }
2764
2765 /**
2766 * Validates the path of a menu link being created or edited.
2767 *
2768 * @return
2769 * TRUE if it is a valid path AND the current user has access permission,
2770 * FALSE otherwise.
2771 */
2772 function menu_valid_path($form_item) {
2773 global $menu_admin;
2774 $item = array();
2775 $path = $form_item['link_path'];
2776 // We indicate that a menu administrator is running the menu access check.
2777 $menu_admin = TRUE;
2778 if ($path == '<front>' || menu_path_is_external($path)) {
2779 $item = array('access' => TRUE);
2780 }
2781 elseif (preg_match('/\/\%/', $path)) {
2782 // Path is dynamic (ie 'user/%'), so check directly against menu_router table.
2783 if ($item = db_query("SELECT * FROM {menu_router} where path = :path", array(':path' => $path))->fetchAssoc()) {
2784 $item['link_path'] = $form_item['link_path'];
2785 $item['link_title'] = $form_item['link_title'];
2786 $item['external'] = FALSE;
2787 $item['options'] = '';
2788 _menu_link_translate($item);
2789 }
2790 }
2791 else {
2792 $item = menu_get_item($path);
2793 }
2794 $menu_admin = FALSE;
2795 return $item && $item['access'];
2796 }
2797
2798 /**
2799 * @} End of "defgroup menu".
2800 */
2801

Legend

Missed
lines code that were not excersized during program execution.
Covered
lines code were excersized during program execution.
Comment/non executable
Comment or non-executable line of code.
Dead
lines of code that according to xdebug could not be executed. This is counted as coverage code because in almost all cases it is code that runnable.