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