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