Package base :: Package includes :: Module menu
[hide private]

Source Code for Module base.includes.menu

   1  #!/usr/bin/env python 
   2  # $Id: menu.inc,v 1.282 2008/07/10 10:58:01 dries Exp $ 
   3   
   4  """ 
   5    API for the Drupal menu system. 
   6   
   7    @package includes 
   8    @see <a href='http://drupy.net'>Drupy Homepage</a> 
   9    @see <a href='http://drupal.org'>Drupal Homepage</a> 
  10    @note Drupy is a port of the Drupal project. 
  11    @note This file was ported from Drupal's includes/menu.inc 
  12    @author Brendon Crawford 
  13    @copyright 2008 Brendon Crawford 
  14    @contact message144 at users dot sourceforge dot net 
  15    @created 2008-05-22 
  16    @version 0.1 
  17    @note License: 
  18   
  19      This program is free software; you can redistribute it and/or 
  20      modify it under the terms of the GNU General Public License 
  21      as published by the Free Software Foundation; either version 2 
  22      of the License, or (at your option) any later version. 
  23   
  24      This program is distributed in the hope that it will be useful, 
  25      but WITHOUT ANY WARRANTY; without even the implied warranty of 
  26      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  27      GNU General Public License for more details. 
  28   
  29      You should have received a copy of the GNU General Public License 
  30      along with this program; if not, write to: 
  31       
  32      The Free Software Foundation, Inc., 
  33      51 Franklin Street, Fifth Floor, 
  34      Boston, MA  02110-1301, 
  35      USA 
  36  """ 
  37   
  38  __version__ = "$Revision: 1 $" 
  39   
  40  from lib.drupy import DrupyPHP as php 
  41   
  42  # 
  43  # 
  44  # @defgroup menu Menu system 
  45  # @{ 
  46  # Define the navigation menus, and route page requests to code based on URLs. 
  47  # 
  48  # The Drupal menu system drives both the navigation system from a user 
  49  # perspective and the callback system that Drupal uses to respond to URLs 
  50  # passed from the browser. For this reason, a good understanding of the 
  51  # menu system is fundamental to the creation of complex plugins. 
  52  # 
  53  # Drupal's menu system follows a simple hierarchy defined by paths. 
  54  # Implementations of hook_menu() define menu items and assign them to 
  55  # paths (which should be unique). The menu system aggregates these items 
  56  # and determines the menu hierarchy from the paths. For example, if the 
  57  # paths defined were a, a/b, e, a/b/c/d, f/g, and a/b/h, the menu system 
  58  # would form the structure: 
  59  # - a 
  60  #   - a/b 
  61  #     - a/b/c/d 
  62  #     - a/b/h 
  63  # - e 
  64  # - f/g 
  65  # Note that the number of elements in the path does not necessarily 
  66  # determine the depth of the menu item in the tree. 
  67  # 
  68  # When responding to a page request, the menu system looks to see if the 
  69  # path requested by the browser is registered as a menu item with a 
  70  # callback. If not, the system searches up the menu tree for the most 
  71  # complete match with a callback it can find. If the path a/b/i is 
  72  # requested in the tree above, the callback for a/b would be used. 
  73  # 
  74  # The found callback function is called with any arguments specified 
  75  # in the "page arguments" attribute of its menu item. The 
  76  # attribute must be an array. After these arguments, any remaining 
  77  # components of the path are appended as further arguments. In this 
  78  # way, the callback for a/b above could respond to a request for 
  79  # a/b/i differently than a request for a/b/j. 
  80  # 
  81  # For an illustration of this process, see page_example.plugin. 
  82  # 
  83  # Access to the callback functions is also protected by the menu system. 
  84  # The "access callback" with an optional "access arguments" of each menu 
  85  # item is called before the page callback proceeds. If this returns True, 
  86  # then access is granted; if False, then access is denied. Menu items may 
  87  # omit this attribute to use the value provided by an ancestor item. 
  88  # 
  89  # In the default Drupal interface, you will notice many links rendered as 
  90  # tabs. These are known in the menu system as "local tasks", and they are 
  91  # rendered as tabs by default, though other presentations are possible. 
  92  # Local tasks function just as other menu items in most respects. It is 
  93  # convention that the names of these tasks should be short verbs if 
  94  # possible. In addition, a "default" local task should be provided for 
  95  # each set. When visiting a local task's parent menu item, the default 
  96  # local task will be rendered as if it is selected; this provides for a 
  97  # normal tab user experience. This default task is special in that it 
  98  # links not to its provided path, but to its parent item's path instead. 
  99  # The default task's path is only used to place it appropriately in the 
 100  # menu hierarchy. 
 101  # 
 102  # Everything described so far is stored in the menu_router table. The 
 103  # menu_links table holds the visible menu links. By default these are 
 104  # derived from the same hook_menu definitions, however you are free to 
 105  # add more with menu_link_save(). 
 106  # 
 107  # 
 108  # @name Menu flags 
 109  # @{ 
 110  # Flags for use in the "type" attribute of menu items. 
 111  # 
 112   
 113  # 
 114  # Internal menu flag -- menu item is the root of the menu tree. 
 115  # 
 116  MENU_IS_ROOT = 0x0001 
 117   
 118  # 
 119  # Internal menu flag -- menu item is visible in the menu tree. 
 120  # 
 121  MENU_VISIBLE_IN_TREE = 0x0002 
 122   
 123  # 
 124  # Internal menu flag -- menu item is visible in the breadcrumb. 
 125  # 
 126  MENU_VISIBLE_IN_BREADCRUMB = 0x0004 
 127   
 128  # 
 129  # Internal menu flag -- menu item links back to its parnet. 
 130  # 
 131   
 132  MENU_LINKS_TO_PARENT = 0x0008 
 133   
 134  # 
 135  # Internal menu flag -- menu item can be modified by administrator. 
 136  # 
 137  MENU_MODIFIED_BY_ADMIN = 0x0020 
 138   
 139  # 
 140  # Internal menu flag -- menu item was created by administrator. 
 141  # 
 142  MENU_CREATED_BY_ADMIN = 0x0040 
 143   
 144  # 
 145  # Internal menu flag -- menu item is a local task. 
 146  # 
 147  MENU_IS_LOCAL_TASK = 0x0080 
 148   
 149   
 150  # 
 151  # @} End of "Menu flags". 
 152  # 
 153  # 
 154  # @name Menu item types 
 155  # @{ 
 156  # Menu item definitions provide one of these constants, which are shortcuts for 
 157  # combinations of the above flags. 
 158  # 
 159  # 
 160  # Menu type -- A "normal" menu item that's shown in menu and breadcrumbs. 
 161  # Normal menu items show up in the menu tree and can be moved/hidden by 
 162  # the administrator. Use this for most menu items. It is the default value if 
 163  # no menu item type is specified. 
 164  # 
 165  MENU_NORMAL_ITEM = MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB 
 166   
 167  # Menu type -- A hidden, internal callback, typically used for API calls. 
 168  # 
 169  # Callbacks simply register a path so that the correct function is fired 
 170  # when the URL is accessed. They are not shown in the menu. 
 171  # 
 172  MENU_CALLBACK = MENU_VISIBLE_IN_BREADCRUMB 
 173   
 174  # Menu type -- A normal menu item, hidden until enabled by an administrator. 
 175  # 
 176  # Modules may "suggest" menu items that the administrator may enable. They act 
 177  # just as callbacks do until enabled, at which time they act like normal items. 
 178  # Note for the value: 0x0010 was a flag which is no longer used, but this way 
 179  # the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate. 
 180  # 
 181  MENU_SUGGESTED_ITEM = MENU_VISIBLE_IN_BREADCRUMB | 0x0010 
 182   
 183  # Menu type -- A task specific to the parent item, usually rendered as a tab. 
 184  # 
 185  # Local tasks are menu items that describe actions to be performed on their 
 186  # parent item. An example is the path "node/52/edit", which performs the 
 187  # "edit" task on "node/52". 
 188  MENU_LOCAL_TASK = MENU_IS_LOCAL_TASK 
 189   
 190  # Menu type -- The "default" local task, which is initially active. 
 191  # 
 192  # Every set of local tasks should provide one "default" task, that links to the 
 193  # same path as its parent when clicked. 
 194  # 
 195  MENU_DEFAULT_LOCAL_TASK = MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT 
 196  # 
 197  # @} End of "Menu item types". 
 198  # 
 199  # 
 200  # @name Menu status codes 
 201  # @{ 
 202  # Status codes for menu callbacks. 
 203  # 
 204   
 205  # 
 206  # Internal menu status code -- Menu item was found. 
 207  # 
 208  MENU_FOUND = 1 
 209   
 210  # 
 211  # Internal menu status code -- Menu item was not found. 
 212  # 
 213  MENU_NOT_FOUND = 2 
 214   
 215  # 
 216  # Internal menu status code -- Menu item access is denied. 
 217  # 
 218  MENU_ACCESS_DENIED = 3 
 219   
 220  # 
 221  # Internal menu status code -- Menu item inaccessible because site is offline. 
 222  # 
 223  MENU_SITE_OFFLINE = 4 
 224   
 225  # 
 226  # @} End of "Menu status codes". 
 227  # 
 228  # 
 229  # @Name Menu tree parameters 
 230  # @{ 
 231  # Menu tree 
 232  # 
 233  # 
 234  # The maximum number of path elements for a menu callback 
 235  # 
 236  MENU_MAX_PARTS = 7 
 237  # 
 238  # The maximum depth of a menu links tree - matches the number of p columns. 
 239  # 
 240  MENU_MAX_DEPTH = 9 
 241   
 242   
243 -def get_ancestors(parts):
244 """ 245 @ End of "Menu tree parameters". 246 Returns the ancestors (and relevant placeholders) for any given path. 247 248 For example, the ancestors of node/12345/edit are: 249 - node/12345/edit 250 - node/12345/% 251 - node/%/edit 252 - node/%/% 253 - node/12345 254 - node/% 255 - node 256 257 To generate these, we will use binary numbers. Each bit represents a 258 part of the path. If the bit is 1, then it represents the original 259 value while 0 means wildcard. If the path is node/12/edit/foo 260 then the 1011 bitstring represents node/%/edit/foo where % means that 261 any argument matches that part. We limit ourselves to using binary 262 numbers that correspond the patterns of wildcards of router items that 263 actually exists. This list of 'masks' is built in menu_rebuild(). 264 265 @param parts 266 An array of path parts, for the above example 267 array('node', '12345', 'edit'). 268 @return 269 An array which contains the ancestors and placeholders. Placeholder 270 simply contain as many '%s' as the ancestors. 271 """ 272 number_parts = php.count(parts) 273 placeholders = [] 274 ancestors = [] 275 length = number_parts - 1 276 end = (1 << number_parts) - 1 277 masks = variable_get('menu_masks', []) 278 # Only examine patterns that actually exist as router items (the masks). 279 for i in masks: 280 if (i > end): 281 # Only look at masks that are not longer than the path of interest. 282 continue 283 elif (i < (1 << length)): 284 # We have exhausted the masks of a given length, so decrease the length. 285 length -= 1 286 current = '' 287 for j in range(length, -1, -1): 288 if (i & (1 << j)): 289 current += parts[length - j] 290 else: 291 current += '%' 292 if (j): 293 current += '/' 294 placeholders.append( "'%s'" ) 295 ancestors.append( current ) 296 return (ancestors, placeholders)
297 298 299
300 -def unserialize(data, map):
301 """ 302 The menu system uses serialized arrays stored in the database for 303 arguments. However, often these need to change according to the 304 current path. This function unserializes such an array and does the 305 necessary change. 306 307 Integer values are mapped according to the map parameter. For 308 example, if php.unserialize(data) is array('view', 1) and map is 309 array('node', '12345') then 'view' will not be changed because 310 it is not an integer, but 1 will as it is an integer. As map[1] 311 is '12345', 1 will be replaced with '12345'. So the result will 312 be array('node_load', '12345'). 313 314 @param @data 315 A serialized array. 316 @param @map 317 An array of potential replacements. 318 @return 319 The data array unserialized and mapped. 320 """ 321 data = php.unserialize(data) 322 if (data): 323 for k,v in data.items(): 324 if (is_int(v)): 325 data[k] = (map[v] if php.isset(map, v) else '') 326 return data 327 else: 328 return []
329 330 331
332 -def set_item(path, router_item):
333 """ 334 Replaces the statically cached item for a given path. 335 @param path 336 The path. 337 @param router_item 338 The router item. Usually you take a router entry from menu_get_item and 339 set it back either modified or to a different path. This lets you modify 340 the navigation block, the page title, the breadcrumb and the page help in 341 one call. 342 """ 343 menu_get_item(path, router_item)
344 345 346
347 -def get_item(path = None, router_item = None):
348 """ 349 Get a router item. 350 351 @param path 352 The path, for example node/5. The function will find the corresponding 353 node/% item and return that. 354 @param router_item 355 Internal use only. 356 @return 357 The router item, an associate array corresponding to one row in the 358 menu_router table. The value of key map holds the loaded objects. The 359 value of key access is True if the current user can access this page. 360 The values for key title, page_arguments, access_arguments will be 361 filled in based on the database values and the objects loaded. 362 """ 363 php.static(menu_get_item, 'router_items') 364 if (not php.isset(path)): 365 path = php.GET['q'] 366 if (router_item != None): 367 menu_get_item.router_items[path] = router_item 368 if (not php.isset(menu_get_item.router_items, path)): 369 original_map = arg(None, path) 370 parts = php.array_slice(original_map, 0, MENU_MAX_PARTS) 371 (ancestors, placeholders) = menu_get_ancestors(parts) 372 router_item = db_fetch_array(db_query_range(\ 373 'SELECT * FROM {menu_router} ' + \ 374 'WHERE path IN (' + implode (',', placeholders) + ') ' + \ 375 'ORDER BY fit DESC', ancestors, 0, 1)) 376 if (router_item): 377 map_ = _menu_translate(router_item, original_map) 378 if (map_ == False): 379 menu_get_item.router_items[path] = False 380 return False 381 if (router_item['access']): 382 router_item['map'] = map 383 router_item['page_arguments'] = \ 384 php.array_merge(menu_unserialize(\ 385 router_item['page_arguments'], map), \ 386 php.array_slice(map, router_item['number_parts'])) 387 menu_get_item.router_items[path] = router_item 388 return menu_get_item.router_items[path]
389 390 391
392 -def execute_active_handler(path = None):
393 """ 394 Execute the page callback associated with the current path 395 """ 396 if (_menu_site_is_offline()): 397 return MENU_SITE_OFFLINE 398 if (variable_get('menu_rebuild_needed', False)): 399 menu_rebuild() 400 router_item = menu_get_item(path) 401 if (router_item): 402 registry_load_path_files() 403 if (router_item['access']): 404 if (drupal_function_exists(router_item['page_callback'])): 405 return router_item['page_callback']( *router_item['page_arguments'] ) 406 else: 407 return MENU_ACCESS_DENIED 408 return MENU_NOT_FOUND
409 410 411
412 -def _load_objects(item, map_):
413 """ 414 Loads objects into the map as defined in the item['load_functions']. 415 416 @param item 417 A menu router or menu link item 418 @param map 419 An array of path arguments (ex: array('node', '5')) 420 @return 421 Returns True for success, False if an object cannot be loaded. 422 Names of object loading functions are placed in item['load_functions']. 423 Loaded objects are placed in map[]; keys are the same as keys in the 424 item['load_functions'] array. 425 item['access'] is set to False if an object cannot be loaded. 426 """ 427 php.Reference.check(item) 428 php.Reference.check(map_) 429 load_functions = item['load_functions'] 430 if (load_functions): 431 # If someone calls this function twice, then unserialize will fail. 432 load_functions_unserialized = php.unserialize(load_functions) 433 if (load_functions_unserialized): 434 load_functions = load_functions_unserialized 435 path_map = map_ 436 for index,function in load_functions.items(): 437 if (function): 438 value = (path_map[index] if php.isset(path_map, index) else '') 439 if (php.is_array(function)): 440 # Set up arguments for the load function. These were pulled from 441 # 'load arguments' in the hook_menu() entry, but they need 442 # some processing. In this case the function is the key to the 443 # load_function array, and the value is the list of arguments. 444 function, args = php.each(function) 445 load_functions[index] = function 446 # Some arguments are placeholders for dynamic items to process. 447 for i,arg in args.items(): 448 if (arg == '%index'): 449 # Pass on argument index to the load function, so multiple 450 # occurances of the same placeholder can be identified. 451 args[i] = index 452 if (arg == '%map'): 453 # Pass on menu map by reference. The accepting function must 454 # also declare this as a reference if it wants to modify 455 # the map. 456 args[i] = map_ 457 if (is_int(arg)): 458 args[i] = (path_map[arg] if php.isset(path_map, arg) else '') 459 array_unshift(args, value) 460 return_ = function(*args) 461 else: 462 return_ = function(value) 463 # If callback returned an error or there is no callback, trigger 404. 464 if (return_ == False): 465 item['access'] = False 466 for i in range(len(map_)): 467 map_.pop() 468 return False 469 map[index] = return_ 470 item['load_functions'] = load_functions 471 return True
472 473 474
475 -def _check_access(item, map_):
476 """ 477 Check access to a menu item using the access callback 478 479 @param item 480 A menu router or menu link item 481 @param map 482 An array of path arguments (ex: array('node', '5')) 483 @return 484 item['access'] becomes True if the item is accessible, False otherwise. 485 """ 486 php.Reference.check(item) 487 # Determine access callback, which will decide whether or not the current 488 # user has access to this path. 489 callback = (0 if php.empty(item['access_callback']) else \ 490 php.trim(item['access_callback'])) 491 # Check for a True or False value. 492 if (php.is_numeric(callback)): 493 item['access'] = p.bool_(callback) 494 else: 495 arguments = menu_unserialize(item['access_arguments'], map_) 496 # As call_user_func_array is quite slow and user_access is a very common 497 # callback, it is worth making a special case for it. 498 if (callback == 'user_access'): 499 item['access'] = (user_access(arguments[0]) if \ 500 (php.count(arguments) == 1) else \ 501 user_access(arguments[0], arguments[1])) 502 else: 503 item['access'] = callback(*arguments)
504 505 506
507 -def _item_localize(item, map_, link_translate = False):
508 """ 509 Localize the router item title using t() or another callback. 510 511 Translate the title and description to allow storage of English title 512 strings in the database, yet display of them in the language required 513 by the current user. 514 515 @param item 516 A menu router item or a menu link item. 517 @param map 518 The path as an array with objects already replaced. E.g., for path 519 node/123 map would be array('node', node) where node is the node 520 object for node 123. 521 @param link_translate 522 True if we are translating a menu link item; False if we are 523 translating a menu router item. 524 @return 525 No return value. 526 item['title'] is localized according to item['title_callback']. 527 If an item's callback is check_plain(), item['options']['html'] becomes 528 True. 529 item['description'] is translated using t(). 530 When doing link translation and the item['options']['attributes']['title'] 531 (link title attribute) matches the description, it is translated as well. 532 """ 533 php.Reference.check(item) 534 callback = item['title_callback'] 535 item['localized_options'] = item['options'] 536 # If we are not doing link translation or if the title matches the 537 # link title of its router item, localize it. 538 if (not link_translate or (not php.empty(item['title']) and \ 539 (item['title'] == item['link_title']))): 540 # t() is a special case. Since it is used very close to all the time, 541 # we handle it directly instead of using indirect, slower methods. 542 if (callback == 't'): 543 if (php.empty(item['title_arguments'])): 544 item['title'] = t(item['title']) 545 else: 546 item['title'] = t(item['title'], \ 547 menu_unserialize(item['title_arguments'], map_)) 548 elif (callback): 549 if (php.empty(item['title_arguments'])): 550 item['title'] = callback(item['title']) 551 else: 552 item['title'] = \ 553 callback(*menu_unserialize(item['title_arguments'], map_)) 554 # Avoid calling check_plain again on l() function. 555 if (callback == 'check_plain'): 556 item['localized_options']['html'] = True 557 elif (link_translate): 558 item['title'] = item['link_title'] 559 # Translate description, see the motivation above. 560 if (not php.empty(item['description'])): 561 original_description = item['description'] 562 item['description'] = t(item['description']) 563 if (link_translate and \ 564 php.isset(item['options']['attributes'], 'title') and \ 565 item['options']['attributes']['title'] == original_description): 566 item['localized_options']['attributes']['title'] = \ 567 item['description']
568 569 570
571 -def _translate(router_item, map_, to_arg = False):
572 """ 573 Handles dynamic path translation and menu access control. 574 575 When a user arrives on a page such as node/5, this function determines 576 what "5" corresponds to, by inspecting the page's menu path definition, 577 node/%node. This will call node_load(5) to load the corresponding node 578 object. 579 580 It also works in reverse, to allow the display of tabs and menu items which 581 contain these dynamic arguments, translating node/%node to node/5. 582 583 Translation of menu item titles and descriptions are done here to 584 allow for storage of English strings in the database, and translation 585 to the language required to generate the current page 586 587 @param router_item 588 A menu router item 589 @param map 590 An array of path arguments (ex: array('node', '5')) 591 @param to_arg 592 Execute item['to_arg_functions'] or not. Use only if you want to render a 593 path from the menu table, for example tabs. 594 @return 595 Returns the map with objects loaded as defined in the 596 item['load_functions. item['access'] becomes True if the item is 597 accessible, False otherwise. item['href'] is set according to the map. 598 If an error occurs during calling the load_functions (like trying to load 599 a non existing node) then this function return False. 600 """ 601 php.Reference.check(router_item) 602 path_map = map_ 603 if (not _menu_load_objects(router_item, map_)): 604 # An error occurred loading an object. 605 router_item['access'] = False 606 return False 607 if (to_arg): 608 _menu_link_map_translate(path_map, router_item['to_arg_functions']) 609 # Generate the link path for the page request or local tasks. 610 link_map = php.explode('/', router_item['path']) 611 for i in range(router_item['number_parts']): 612 if (link_map[i] == '%'): 613 link_map[i] = path_map[i] 614 router_item['href'] = php.implode('/', link_map) 615 router_item['options'] = {} 616 _menu_check_access(router_item, map_) 617 # For performance, don't localize an item the user can't access. 618 if (router_item['access']): 619 _menu_item_localize(router_item, map_) 620 return map_
621 622 623 646 647 648
649 -def tail_to_arg(arg, map_, index):
650 return php.implode('/', php.array_slice(map_, index))
651 652 653 704 705 706
707 -def get_object(type_ = 'node', position = 1, path = None):
708 """ 709 Get a loaded object from a router item. 710 711 menu_get_object() will provide you the current node on paths like node/5, 712 node/5/revisions/48 etc. menu_get_object('user') will give you the user 713 account on user/5 etc. Note - this function should never be called within a 714 _to_arg function (like user_current_to_arg()) since this may result in an 715 infinite recursion. 716 717 @param type 718 Type of the object. These appear in hook_menu definitons as %type. Core 719 provides aggregator_feed, aggregator_category, contact, filter_format, 720 forum_term, menu, menu_link, node, taxonomy_vocabulary, user. See the 721 relevant {type}_load function for more on each. Defaults to node. 722 @param position 723 The expected position for type object. For node/%node this is 1, for 724 comment/reply/%node this is 2. Defaults to 1. 725 @param path 726 See menu_get_item() for more on this. Defaults to the current path. 727 """ 728 router_item = menu_get_item(path) 729 if (php.isset(router_item['load_functions'], position) and not \ 730 php.empty(router_item['map'][position]) and \ 731 router_item['load_functions'][position] == type_ + '_load'): 732 return router_item['map'][position]
733 734 735
736 -def tree(menu_name = 'navigation'):
737 """ 738 Render a menu tree based on the current path. 739 740 The tree is expanded based on the current path and dynamic paths are also 741 changed according to the defined to_arg functions 742 (for example the 'My account' 743 link is changed from user/% to a link with the current user's uid). 744 745 @param menu_name 746 The name of the menu. 747 @return 748 The rendered HTML of that menu on the current page. 749 """ 750 php.static(menu_tree, 'menu_output', {}) 751 if (not php.isset(menu_tree.menu_output, menu_name)): 752 tree = menu_tree_page_data(menu_name) 753 menu_tree.menu_output[menu_name] = menu_tree_output(tree) 754 return menu_tree.menu_output[menu_name]
755 756 757
758 -def tree_output(tree):
759 """ 760 Returns a rendered menu tree. 761 762 @param tree 763 A data structure representing the tree as returned from menu_tree_data. 764 @return 765 The rendered HTML of that data structure. 766 """ 767 output = '' 768 items = {} 769 # Pull out just the menu items we are going to render so that we 770 # get an accurate count for the first/last classes. 771 for data in tree: 772 if (not data['link']['hidden']): 773 items.append( data ) 774 num_items = php.count(items) 775 for i,data in items.items(): 776 extra_class = None 777 if (i == 0): 778 extra_class = 'first' 779 if (i == num_items - 1): 780 extra_class = 'last' 781 link = theme('menu_item_link', data['link']) 782 if (data['below']): 783 output += theme('menu_item', link, data['link']['has_children'], \ 784 menu_tree_output(data['below']), \ 785 data['link']['in_active_trail'], extra_class) 786 else: 787 output += theme('menu_item', link, data['link']['has_children'], \ 788 '', data['link']['in_active_trail'], extra_class) 789 return (theme('menu_tree', output) if output else '')
790 791 792
793 -def tree_all_data(menu_name = 'navigation', item = None):
794 """ 795 Get the data structure representing a named menu tree. 796 797 Since this can be the full tree including hidden items, the data returned 798 may be used for generating an an admin interface or a select. 799 800 @param menu_name 801 The named menu links to return 802 @param item 803 A fully loaded menu link, or None. If a link is supplied, only the 804 path to root will be included in the returned tree- as if this link 805 represented the current page in a visible menu. 806 @return 807 An tree of menu links in an array, in the order they should be rendered. 808 """ 809 php.static(menu_tree_all_data, 'tree', {}) 810 data = None 811 # Use mlid as a flag for whether the data being loaded is for the whole tree. 812 mlid = (item['mlid'] if php.isset(item, 'mlid') else 0) 813 # Generate a cache ID (cid) specific for this menu_name and item. 814 cid = 'links:' + menu_name + ':all-cid:' + mlid 815 if (not php.isset(menu_tree_all_data.tree, cid)): 816 # If the static variable doesn't have the data, check {cache_menu}. 817 cache = cache_get(cid, 'cache_menu') 818 if (cache and php.isset(cache, 'data')): 819 # If the cache entry exists, it will just be the cid for the actual data. 820 # This avoids duplication of large amounts of data. 821 cache = cache_get(cache.data, 'cache_menu') 822 if (cache and php.isset(cache, 'data')): 823 data = cache.data 824 # If the tree data was not in the cache, data will be None. 825 if (not php.isset(data)): 826 # Build and run the query, and build the tree. 827 if (mlid): 828 # The tree is for a single item, so we need to match the values in its 829 # p columns and 0 (the top level) with the plid values of other links. 830 args = [0] 831 for i in range(1, MENU_MAX_DEPTH): 832 args.append( item["pi"] ) 833 args = array_unique(args) 834 placeholders = php.implode(', ', php.array_fill(0, \ 835 php.count(args), '%d')) 836 where = ' AND ml.plid IN (' + placeholders + ')' 837 parents = args 838 parents.append( item['mlid'] ) 839 else: 840 # Get all links in this menu. 841 where = '' 842 args = {} 843 parents = {} 844 array_unshift(args, menu_name) 845 # Select the links from the table, and recursively build the tree. We 846 # LEFT JOIN since there is no match in {menu_router} for an external 847 # link. 848 data['tree'] = menu_tree_data(db_query( \ 849 "SELECT m.load_functions, m.to_arg_functions, m.access_callback, " + \ 850 "m.access_arguments, m.page_callback, m.page_arguments, m.title, " + \ 851 "m.title_callback, m.title_arguments, m.type, m.description, ml.* " + \ 852 "FROM {menu_links} ml " + \ 853 "LEFT JOIN {menu_router} m ON m.path = ml.router_path " + \ 854 "WHERE ml.menu_name = '%s'" + where + \ 855 "ORDER BY p1 ASC, p2 ASC, p3 ASC, " + \ 856 " p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, " + \ 857 " p9 ASC", \ 858 args), parents) 859 data['node_links'] = {} 860 menu_tree_collect_node_links(data['tree'], data['node_links']) 861 # Cache the data, if it is not already in the cache. 862 tree_cid = _menu_tree_cid(menu_name, data) 863 if (not cache_get(tree_cid, 'cache_menu')): 864 cache_set(tree_cid, data, 'cache_menu') 865 # Cache the cid of the (shared) data 866 # using the menu and item-specific cid. 867 cache_set(cid, tree_cid, 'cache_menu') 868 # Check access for the current user to each item in the tree. 869 menu_tree_check_access(data['tree'], data['node_links']) 870 menu_tree_all_data.tree[cid] = data['tree'] 871 return menu_tree_all_data.tree[cid]
872 873 874
875 -def tree_page_data(menu_name = 'navigation'):
876 """ 877 Get the data structure representing a named 878 menu tree, based on the current page. 879 880 The tree order is maintained by storing each parent in an individual 881 field, see http://drupal.org/node/141866 for more. 882 883 @param menu_name 884 The named menu links to return 885 @return 886 An array of menu links, in the order they should be rendered. The array 887 is a list of associative arrays -- these have two keys, link and below. 888 link is a menu item, ready for theming as a link. Below represents the 889 submenu below the link if there is one, and it is a subtree that has the 890 same structure described for the top-level array. 891 """ 892 php.static(menu_tree_page_data, 'tree', {}) 893 # Load the menu item corresponding to the current page. 894 data = None 895 item = menu_get_item() 896 if (item): 897 # Generate a cache ID (cid) specific for this page. 898 cid = 'links:' + menu_name + ':page-cid:' + item['href'] + ':' + \ 899 int(item['access']) 900 if (not php.isset(tree, cid)): 901 # If the static variable doesn't have the data, check {cache_menu}. 902 cache = cache_get(cid, 'cache_menu') 903 if (cache and php.isset(cache, 'data')): 904 # If the cache entry exists, it will just be the cid 905 # for the actual data. 906 # This avoids duplication of large amounts of data. 907 cache = cache_get(cache.data, 'cache_menu') 908 if (cache and php.isset(cache, 'data')): 909 data = cache.data 910 # If the tree data was not in the cache, data will be None. 911 if (not php.isset(data)): 912 # Build and run the query, and build the tree. 913 if (item['access']): 914 # Check whether a menu link exists that corresponds 915 # to the current path. 916 args = array(menu_name, item['href']) 917 placeholders = "'%s'" 918 if (drupal_is_front_page()): 919 args.append( '<front>' ) 920 placeholders += ", '%s'" 921 parents = db_fetch_array(db_query(\ 922 "SELECT p1, p2, p3, p4, p5, p6, p7, p8 " + \ 923 "FROM {menu_links} " + \ 924 "WHERE menu_name = '%s' AND " + \ 925 "link_path IN (" + placeholders + ")", args)) 926 if (php.empty(parents)): 927 # If no link exists, we may be on a local task 928 # that's not in the links. 929 # TODO: Handle the case like a local task on a 930 # specific node in the menu. 931 parents = db_fetch_array(db_query(\ 932 "SELECT p1, p2, p3, p4, p5, p6, p7, p8 " + \ 933 "FROM {menu_links} " + \ 934 "WHERE menu_name = '%s' AND " + \ 935 "link_path = '%s'", menu_name, item['tab_root'])) 936 # We always want all the top-level links with plid == 0. 937 parents.append( '0' ) 938 # Use array_values() so that the indices 939 # are numeric for php.array_merge(). 940 args = parents = array_unique(array_values(parents)) 941 placeholders = php.implode(', ', php.array_fill(0, \ 942 php.count(args), '%d')) 943 expanded = variable_get('menu_expanded', []) 944 # Check whether the current menu has any links set to be expanded. 945 if (php.in_array(menu_name, expanded)): 946 # Collect all the links set to be expanded, and then add all of 947 # their children to the list as well. 948 while True: 949 result = db_query( \ 950 "SELECT mlid FROM {menu_links} WHERE menu_name = '%s' " + \ 951 "AND expanded = 1 AND has_children = 1 AND plid IN (" + \ 952 placeholders + \ 953 ") AND mlid NOT IN (" + \ 954 placeholders + \ 955 ")", php.array_merge(array(menu_name), args, args)) 956 num_rows = False 957 while True: 958 item = db_fetch_array(result) 959 if not item: 960 break 961 args.append( item['mlid'] ) 962 num_rows = True 963 placeholders = php.implode(', ', php.array_fill(0, \ 964 php.count(args), '%d')) 965 if not num_rows: 966 break 967 array_unshift(args, menu_name) 968 else: 969 # Show only the top-level menu items when access is denied. 970 args = array(menu_name, '0') 971 placeholders = '%d' 972 parents = {} 973 # Select the links from the table, and recursively build the tree. We 974 # LEFT JOIN since there is no match in {menu_router} for an external 975 # link. 976 data['tree'] = menu_tree_data(db_query( \ 977 "SELECT " + \ 978 " m.load_functions, m.to_arg_functions, m.access_callback, " + \ 979 " m.access_arguments, m.page_callback, " + \ 980 " m.page_arguments, m.title, m.title_callback, " + \ 981 " m.title_arguments, m.type, m.description, ml.* " + \ 982 "FROM {menu_links} ml " + \ 983 "LEFT JOIN {menu_router} m ON m.path = ml.router_path " + \ 984 "WHERE ml.menu_name = '%s' AND ml.plid IN " + \ 985 " (" + placeholders + ") " + \ 986 "ORDER BY p1 ASC, p2 ASC, p3 ASC, " + \ 987 " p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, " + \ 988 " p9 ASC", \ 989 args), parents) 990 data['node_links'] = {} 991 menu_tree_collect_node_links(data['tree'], data['node_links']) 992 # Cache the data, if it is not already in the cache. 993 tree_cid = _menu_tree_cid(menu_name, data) 994 if (not cache_get(tree_cid, 'cache_menu')): 995 cache_set(tree_cid, data, 'cache_menu') 996 # Cache the cid of the (shared) data using the page-specific cid. 997 cache_set(cid, tree_cid, 'cache_menu') 998 # Check access for the current user to each item in the tree. 999 menu_tree_check_access(data['tree'], data['node_links']) 1000 menu_tree_page_data.tree[cid] = data['tree'] 1001 return menu_tree_page_data.tree[cid] 1002 return {}
1003 1004 1005
1006 -def _tree_cid(menu_name, data):
1007 """ 1008 Helper function - compute the real cache ID for menu tree data. 1009 """ 1010 return 'links:' + menu_name + ':tree-data:' + php.md5(php.serialize(data))
1011 1012 1013 1028 1029 1030
1031 -def tree_check_access(tree, node_links = {}):
1032 """ 1033 Check access and perform other dynamic operations for each link in the tree. 1034 """ 1035 php.Reference.check(tree) 1036 if (not php.empty(node_links)): 1037 # Use db_rewrite_sql to evaluate view access 1038 # without loading each full node. 1039 nids = php.array_keys(node_links) 1040 placeholders = '%d' + str_repeat(', %d', php.count(nids) - 1) 1041 result = db_query(db_rewrite_sql(\ 1042 "SELECT n.nid FROM {node} n " + \ 1043 "WHERE n.status = 1 AND n.nid IN (" + placeholders + ")"), nids) 1044 while True: 1045 node = db_fetch_array(result) 1046 if not node: 1047 break 1048 nid = node['nid'] 1049 for mlid,link in node_links[nid].items(): 1050 node_links[nid][mlid]['access'] = True 1051 _menu_tree_check_access(tree) 1052 return
1053 1054 1055
1056 -def _tree_check_access(tree):
1057 """ 1058 Recursive helper function for menu_tree_check_access() 1059 """ 1060 php.Reference.check(tree) 1061 new_tree = {} 1062 for key,v in tree.items(): 1063 item = tree[key]['link'] 1064 _menu_link_translate(item) 1065 if (item['access']): 1066 if (tree[key]['below']): 1067 _menu_tree_check_access(tree[key]['below']) 1068 # The weights are made a uniform 5 digits by adding 50000 as an offset. 1069 # After _menu_link_translate(), item['title'] has 1070 # the localized link title. 1071 # Adding the mlid to the end of the index insures that it is unique. 1072 new_tree[(50000 + item['weight']) + ' ' + \ 1073 item['title'] + ' ' + item['mlid']] = tree[key] 1074 # Sort siblings in the tree based on the weights and localized titles. 1075 ksort(new_tree) 1076 php.array_merge(tree, new_tree, True)
1077 1078 1079
1080 -def tree_data(result = None, parents = {}, depth = 1):
1081 """ 1082 Build the data representing a menu tree. 1083 1084 @param result 1085 The database result. 1086 @param parents 1087 An array of the plid values that represent the path from the current page 1088 to the root of the menu tree. 1089 @param depth 1090 The depth of the current menu tree. 1091 @return 1092 See menu_tree_page_data for a description of the data structure. 1093 """ 1094 tree = _menu_tree_data(result, parents, depth)[1] 1095 return tree
1096 1097 1098
1099 -def _tree_data(result, parents, depth, previous_element = ''):
1100 """ 1101 Recursive helper function to build the data representing a menu tree. 1102 1103 The function is a bit complex because the rendering of an item depends on 1104 the next menu item. So we are always rendering the element previously 1105 processed not the current one. 1106 """ 1107 remnant = None 1108 tree = {} 1109 while True: 1110 item = db_fetch_array(result) 1111 if not item: 1112 break 1113 # We need to determine if we're on the path to root so we can later build 1114 # the correct active trail and breadcrumb. 1115 item['in_active_trail'] = php.in_array(item['mlid'], parents) 1116 # The current item is the first in a new submenu. 1117 if (item['depth'] > depth): 1118 # _menu_tree returns an item and the menu tree structure. 1119 item, below = _menu_tree_data(result, parents, item['depth'], item) 1120 if (previous_element): 1121 tree[previous_element['mlid']] = { 1122 'link': previous_element, 1123 'below': below, 1124 } 1125 else: 1126 tree = below 1127 # We need to fall back one level. 1128 if (item == None or item['depth'] < depth): 1129 return [item, tree] 1130 # This will be the link to be output in the next iteration. 1131 previous_element = item 1132 # We are at the same depth, so we use the previous element. 1133 elif (item['depth'] == depth): 1134 if (previous_element): 1135 # Only the first time. 1136 tree[previous_element['mlid']] = { 1137 'link': previous_element, 1138 'below': False, 1139 } 1140 # This will be the link to be output in the next iteration. 1141 previous_element = item 1142 # The submenu ended with the previous item, so pass back the current item. 1143 else: 1144 remnant = item 1145 break 1146 if (previous_element): 1147 # We have one more link dangling. 1148 tree[previous_element['mlid']] = { 1149 'link': previous_element, 1150 'below': False, 1151 } 1152 return [remnant, tree]
1153 1154 1155 1165 1166 1167
1168 -def theme_menu_tree(tree):
1169 """ 1170 Generate the HTML output for a menu tree 1171 1172 @ingroup themeable 1173 """ 1174 return '<ul class="menu">' + tree + '</ul>'
1175 1176 1177
1178 -def theme_menu_item(link, has_children, menu = '', \ 1179 in_active_trail = False, extra_class = None):
1180 """ 1181 Generate the HTML output for a menu item and submenu. 1182 1183 @ingroup themeable 1184 """ 1185 class_ = ('expanded' if not php.empty(menu) else \ 1186 ('collapsed' if has_children else 'leaf')) 1187 if (not php.empty(extra_class)): 1188 class_ += ' ' + extra_class 1189 if (in_active_trail): 1190 class_ += ' active-trail' 1191 return '<li class="' + class_ + '">' + link + menu + "</li>\n"
1192 1193 1194 1195
1196 -def theme_menu_local_task(link, active = False):
1197 """ 1198 Generate the HTML output for a single local task link. 1199 1200 @ingroup themeable 1201 """ 1202 return '<li ' + ('class="active" ' if active else '') + \ 1203 '>' + link + "</li>\n"
1204 1205 1206
1207 -def drupal_help_arg(arg = []):
1208 """ 1209 Generates elements for the arg array in the help hook. 1210 """ 1211 # Note - the number of empty elements should be > MENU_MAX_PARTS. 1212 return arg + ['', '', '', '', '', '', '', '', '', '', '', '']
1213 1214 1215
1216 -def get_active_help():
1217 """ 1218 Returns the help associated with the active menu item. 1219 """ 1220 output = '' 1221 router_path = menu_tab_root_path() 1222 arg = drupal_help_arg(arg(None)) 1223 empty_arg = drupal_help_arg() 1224 for name in plugin_list(): 1225 if (plugin_hook(name, 'help')): 1226 # Lookup help for this path. 1227 help = plugin_invoke(name, 'help', router_path, arg) 1228 if (help): 1229 output += help + "\n" 1230 # Add "more help" link on admin pages if the plugin provides a 1231 # standalone help page. 1232 if (arg[0] == "admin" and plugin_exists('help') and plugin_invoke(name, \ 1233 'help', 'admin/help#' + arg[2], empty_arg) and help): 1234 output += theme("more_help_link", url('admin/help/' + arg[2])) 1235 return output
1236 1237 1238 1239
1240 -def get_names(reset = False):
1241 """ 1242 Build a list of named menus. 1243 """ 1244 php.static(menu_get_names, 'names', []) 1245 if (reset or php.empty(menu_get_names.names)): 1246 result = db_query(\ 1247 "SELECT DISTINCT(menu_name) FROM {menu_links} ORDER BY menu_name") 1248 while True: 1249 name = db_fetch_array(result) 1250 if name == None: 1251 break 1252 menu_get_names.names.append(name['menu_name']) 1253 return menu_get_names.names
1254 1255 1256
1257 -def list_system_menus():
1258 """ 1259 Return an array containing the names of system-defined (default) menus. 1260 """ 1261 return ('navigation', 'main-menu', 'secondary-menu')
1262 1263 1270 1271
1272 -def secondary_menu():
1273 """ 1274 Return an array of links to be rendered as the Secondary links. 1275 """ 1276 # If the secondary menu source is set as the primary menu, we display the 1277 # second level of the primary menu. 1278 if (variable_get('menu_secondary_menu_source', 'secondary-menu') == \ 1279 variable_get('menu_main_menu_source', 'main-menu')): 1280 return menu_navigation_links(\ 1281 variable_get('menu_main_menu_source', 'main-menu'), 1) 1282 else: 1283 return menu_navigation_links(\ 1284 variable_get('menu_secondary_menu_source', 'secondary-menu'), 0)
1285 1286 1331 1332 1333
1334 -def local_tasks(level = 0, return_root = False):
1335 """ 1336 Collects the local tasks (tabs) for a given level. 1337 1338 @param level 1339 The level of tasks you ask for. Primary tasks are 0, secondary are 1. 1340 @param return_root 1341 Whether to return the root path for the current page. 1342 @return 1343 Themed output corresponding to the tabs of the requested level, or 1344 router path if return_root == True. This router path corresponds to 1345 a parent tab, if the current page is a default local task. 1346 """ 1347 php.static(menu_local_tasks, 'tabs', []) 1348 php.static(menu_local_tasks, 'root_path') 1349 if (php.empty(menu_local_tasks.tabs)): 1350 router_item = menu_get_item() 1351 if (not router_item or not router_item['access']): 1352 return '' 1353 # Get all tabs and the root page. 1354 result = db_query(\ 1355 "SELECT * FROM {menu_router} " + \ 1356 "WHERE tab_root = '%s' ORDER BY weight, title", router_item['tab_root']) 1357 map_ = arg() 1358 children = [] 1359 tasks = [] 1360 menu_local_tasks.rootpath = router_item['path'] 1361 while True: 1362 item = db_fetch_array(result) 1363 if item == None: 1364 break 1365 _menu_translate(item, map_, True) 1366 if (item['tab_parent']): 1367 # All tabs, but not the root page. 1368 children[item['tab_parent']][item['path']] = item 1369 # Store the translated item for later use. 1370 tasks[item['path']] = item 1371 # Find all tabs below the current path. 1372 path = router_item['path'] 1373 # Tab parenting may skip levels, so the number of parts in the path may not 1374 # equal the depth. Thus we use the depth counter 1375 # (offset by 1000 for ksort). 1376 depth = 1001 1377 while (php.isset(children, path)): 1378 tabs_current = '' 1379 next_path = '' 1380 count = 0 1381 for item in children[path]: 1382 if (item['access']): 1383 count += 1 1384 # The default task is always active. 1385 if (item['type'] == MENU_DEFAULT_LOCAL_TASK): 1386 # Find the first parent which is not a default local task. 1387 p = item['tab_parent'] 1388 while True: 1389 if tasks[p]['type'] != MENU_DEFAULT_LOCAL_TASK: 1390 break 1391 p = tasks[p]['tab_parent'] 1392 link = theme('menu_item_link', {'href' : tasks[p]['href']} + item) 1393 tabs_current += theme('menu_local_task', link, True) 1394 next_path = item['path'] 1395 else: 1396 link = theme('menu_item_link', item) 1397 tabs_current += theme('menu_local_task', link) 1398 path = next_path 1399 menu_local_tasks.tabs[depth]['count'] = count 1400 menu_local_tasks.tabs[depth]['output'] = tabs_current 1401 depth += 1 1402 # Find all tabs at the same level or above the current one. 1403 parent = router_item['tab_parent'] 1404 path = router_item['path'] 1405 current = router_item 1406 depth = 1000 1407 while (php.isset(children, parent)): 1408 tabs_current = '' 1409 next_path = '' 1410 next_parent = '' 1411 count = 0 1412 for item in children[parent]: 1413 if (item['access']): 1414 count += 1 1415 if (item['type'] == MENU_DEFAULT_LOCAL_TASK): 1416 # Find the first parent which is not a default local task. 1417 p = item['tab_parent'] 1418 while True: 1419 if tasks[p]['type'] != MENU_DEFAULT_LOCAL_TASK: 1420 break 1421 p = tasks[p]['tab_parent'] 1422 link = theme('menu_item_link', {'href' : tasks[p]['href']} + item) 1423 if (item['path'] == router_item['path']): 1424 menu_local_tasks.root_path = tasks[p]['path'] 1425 else: 1426 link = theme('menu_item_link', item) 1427 # We check for the active tab. 1428 if (item['path'] == path): 1429 tabs_current += theme('menu_local_task', link, True) 1430 next_path = item['tab_parent'] 1431 if (php.isset(tasks, next_path)): 1432 next_parent = tasks[next_path]['tab_parent'] 1433 else: 1434 tabs_current += theme('menu_local_task', link) 1435 path = next_path 1436 parent = next_parent 1437 menu_local_tasks.tabs[depth]['count'] = count 1438 menu_local_tasks.tabs[depth]['output'] = tabs_current 1439 depth -= 1 1440 # Sort by depth. 1441 menu_local_tasks.tabs = ksort(menu_local_tasks.tabs) 1442 # Remove the depth, we are interested only in their relative placement. 1443 menu_local_tasks.tabs = array_values(menu_local_tasks.tabs.tabs) 1444 if (return_root): 1445 return menu_local_tasks.rootpath 1446 else: 1447 # We do not display single tabs. 1448 return (menu_local_tasks.tabs[level]['output'] if \ 1449 (php.isset(menu_local_tasks.tabs, level) and \ 1450 menu_local_tasks.tabs[level]['count'] > 1) else '')
1451 1452 1453
1454 -def primary_local_tasks():
1455 """ 1456 Returns the rendered local tasks at the top level. 1457 """ 1458 return menu_local_tasks(0)
1459 1460 1461
1462 -def secondary_local_tasks():
1463 """ 1464 Returns the rendered local tasks at the second level. 1465 """ 1466 return menu_local_tasks(1)
1467 1468 1469
1470 -def tab_root_path():
1471 """ 1472 Returns the router path, or the path of the parent tab of a default 1473 local task. 1474 """ 1475 return menu_local_tasks(0, True)
1476 1477 1478
1479 -def theme_menu_local_tasks():
1480 """ 1481 Returns the rendered local tasks. The default implementation renders 1482 them as tabs. 1483 1484 @ingroup themeable 1485 """ 1486 output = '' 1487 primary = menu_primary_local_tasks() 1488 if (primary): 1489 output += "<ul class=\"tabs primary\">\n" + primary + "</ul>\n" 1490 secondary = menu_secondary_local_tasks() 1491 if (secondary): 1492 output += "<ul class=\"tabs secondary\">\n" + secondary + "</ul>\n" 1493 return output
1494 1495 1496
1497 -def set_active_menu_name(menu_name = None):
1498 """ 1499 Set (or get) the active menu for the current page - 1500 determines the active trail. 1501 """ 1502 php.static(menu_set_active_menu_name, 'active') 1503 if (menu_name != None): 1504 menu_set_active_menu_name.active = menu_name 1505 elif (menu_set_active_menu_name.active == None): 1506 menu_set_active_menu_name.active = 'navigation' 1507 return menu_set_active_menu_name.active
1508 1509 1510 1511
1512 -def get_active_menu_name():
1513 """ 1514 Get the active menu for the current page - determines the active trail. 1515 """ 1516 return menu_set_active_menu_name()
1517 1518 1519
1520 -def set_active_item(path):
1521 """ 1522 Set the active path, which determines which page is loaded. 1523 1524 @param path 1525 A Drupal path - not a path alias. 1526 1527 Note that this may not have the desired effect unless invoked very early 1528 in the page load, such as during hook_boot, or unless you call 1529 menu_execute_active_handler() to generate your page output. 1530 """ 1531 php.GET['q'] = path
1532 1533 1534
1535 -def set_active_trail(new_trail = None):
1536 """ 1537 Set (or get) the active trail for the current page - 1538 the path to root in the menu tree. 1539 """ 1540 php.static(menu_set_active_trail, 'trail') 1541 if (new_trail != None): 1542 static_menusetactivetrail_trail = new_trail 1543 elif (static_menusetactivetrail_trail == None): 1544 menu_set_active_trail.trail = [] 1545 menu_set_active_trail.trail.append( {'title': \ 1546 t('Home'), 'href': '<front>', 'localized_options': {}, 'type': 0}) 1547 item = menu_get_item() 1548 # Check whether the current item is a local task (displayed as a tab). 1549 if (item['tab_parent']): 1550 # The title of a local task is used for the tab, never the page title. 1551 # Thus, replace it with the item corresponding to the root path to get 1552 # the relevant href and title. For example, the menu item corresponding 1553 # to 'admin' is used when on the 'By plugin' tab at 'admin/by-plugin'. 1554 parts = php.explode('/', item['tab_root']) 1555 args = arg() 1556 # Replace wildcards in the root path using the current path. 1557 for index,part in parts.items(): 1558 if (part == '%'): 1559 parts[index] = args[index] 1560 # Retrieve the menu item using the root path after wildcard replacement. 1561 root_item = menu_get_item(php.implode('/', parts)) 1562 if (root_item and root_item['access']): 1563 item = root_item 1564 tree = menu_tree_page_data(menu_get_active_menu_name()) 1565 key,curr = each(tree) 1566 while (curr): 1567 # Terminate the loop when we find the current path in the active trail. 1568 if (curr['link']['href'] == item['href']): 1569 menu_set_active_trail.trail.append( curr['link'] ) 1570 curr = False 1571 else: 1572 # Add the link if it's in the active trail, then 1573 # move to the link below. 1574 if (curr['link']['in_active_trail']): 1575 menu_set_active_trail.trail.append( curr['link'] ) 1576 tree = (curr['below'] if curr['below'] else []); 1577 key,curr = p.each(tree) 1578 # Make sure the current page is in the trail (needed for the page title), 1579 # but exclude tabs and the front page. 1580 last = php.count(menu_set_active_trail.trail) - 1 1581 if (menu_set_active_trail.trail[last]['href'] != item['href'] and not \ 1582 drupy_bool(item['type'] & MENU_IS_LOCAL_TASK) and \ 1583 not drupal_is_front_page()): 1584 menu_set_active_trail.trail.append( item ) 1585 return menu_set_active_trail.trail
1586 1587 1588
1589 -def get_active_trail():
1590 """ 1591 Get the active trail for the current page - the path to 1592 root in the menu tree. 1593 """ 1594 return menu_set_active_trail()
1595 1596 1597
1598 -def get_active_breadcrumb():
1599 """ 1600 Get the breadcrumb for the current page, as determined by the active trail. 1601 """ 1602 breadcrumb = {} 1603 # No breadcrumb for the front page. 1604 if (drupal_is_front_page()): 1605 return breadcrumb 1606 item = menu_get_item() 1607 if (item and item['access']): 1608 active_trail = menu_get_active_trail() 1609 for parent in active_trail: 1610 breadcrumb.append( l(parent['title'], parent['href'], \ 1611 parent['localized_options']) ) 1612 end = end(active_trail) 1613 # Don't show a link to the current page in the breadcrumb trail. 1614 if (item['href'] == end['href'] or \ 1615 (item['type'] == MENU_DEFAULT_LOCAL_TASK and \ 1616 end['href'] != '<front>')): 1617 php.array_pop(breadcrumb) 1618 return breadcrumb
1619 1620 1621
1622 -def get_active_title():
1623 """ 1624 Get the title of the current page, as determined by the active trail. 1625 """ 1626 active_trail = menu_get_active_trail() 1627 for item in php.array_reverse(active_trail): 1628 if (not drupy_bool(item['type'] & MENU_IS_LOCAL_TASK)): 1629 return item['title']
1630 1631 1632 1656 1657 1658
1659 -def cache_clear(menu_name = 'navigation'):
1660 """ 1661 Clears the cached cached data for a single named menu. 1662 """ 1663 php.static(menu_cache_clear, 'cache_cleared', {}) 1664 if (not php.isset(menu_cache_clear.cache_cleared, menu_name) or \ 1665 php.empty(menu_cache_clear.cache_cleared[menu_name])): 1666 cache_clear_all('links:' + menu_name + ':', 'cache_menu', True) 1667 menu_cache_clear.cache_cleared[menu_name] = 1 1668 elif (menu_cache_clear.cache_cleared[menu_name] == 1): 1669 register_shutdown_function('cache_clear_all', 'links:' + \ 1670 menu_name + ':', 'cache_menu', True) 1671 menu_cache_clear.cache_cleared[menu_name] = 2
1672 1673 1674
1675 -def cache_clear_all():
1676 """ 1677 Clears all cached menu data. This should be called any time broad changes 1678 might have been made to the router items or menu links. 1679 """ 1680 cache_clear_all('*', 'cache_menu', True)
1681 1682 1683
1684 -def rebuild():
1685 """ 1686 (Re)populate the database tables used by various menu functions. 1687 1688 This function will clear and populate the {menu_router} table, add entries 1689 to {menu_links} for new router items, then remove stale items from 1690 {menu_links}. If called from update.php or install.php, it will also 1691 schedule a call to itself on the first real page load from 1692 menu_execute_active_handler(), because the maintenance page environment 1693 is different and leaves stale data in the menu tables. 1694 """ 1695 variable_del('menu_rebuild_needed') 1696 menu_cache_clear_all() 1697 menu = menu_router_build(True) 1698 _menu_navigation_links_rebuild(menu) 1699 # Clear the page and block caches. 1700 _menu_clear_page_cache() 1701 if (php.defined('MAINTENANCE_MODE')): 1702 variable_set('menu_rebuild_needed', True)
1703 1704 1705
1706 -def router_build(reset = False):
1707 """ 1708 Collect, alter and store the menu definitions. 1709 """ 1710 php.static(menu_router_build, 'menu') 1711 if (menu_router_build.menu == None or reset): 1712 cache = cache_get('router:', 'cache_menu') 1713 if (not reset and cache and php.isset(cache, 'data')): 1714 menu_router_build.menu = cache.data 1715 else: 1716 # We need to manually call each plugin so that we can know which plugin 1717 # a given item came from. 1718 callbacks = [] 1719 for plugin in plugin_implements('menu', None, True): 1720 router_items = (plugin+'_menu')() 1721 if (router_items != None and php.is_array(router_items)): 1722 for path in php.array_keys(router_items): 1723 router_items[path]['plugin'] = plugin 1724 callbacks = php.array_merge(callbacks, router_items) 1725 # Alter the menu as defined in plugins, keys are like user/%user. 1726 drupal_alter('menu', callbacks) 1727 menu_router_build.menu = _menu_router_build(callbacks) 1728 return menu_router_build.menu
1729 1730 1731 1752 1753 1754 1821 1822 1823 1843 1844 1845
1846 -def _delete_item(item, force = False):
1847 """ 1848 Helper function for menu_link_delete; deletes a single menu link. 1849 1850 @param item 1851 Item to be deleted. 1852 @param force 1853 Forces deletion. Internal use only, setting to True is discouraged. 1854 """ 1855 if (item and (item['plugin'] != 'system' or item['updated'] or force)): 1856 # Children get re-attached to the item's parent. 1857 if (item['has_children']): 1858 result = db_query("SELECT mlid FROM {menu_links} WHERE plid = %d", \ 1859 item['mlid']) 1860 while True: 1861 m = db_fetch_array(result) 1862 if not m: 1863 break 1864 child = menu_link_load(m['mlid']) 1865 child['plid'] = item['plid'] 1866 menu_link_save(child) 1867 db_query('DELETE FROM {menu_links} WHERE mlid = %d', item['mlid']) 1868 # Update the has_children status of the parent. 1869 _menu_update_parental_status(item) 1870 menu_cache_clear(item['menu_name']) 1871 _menu_clear_page_cache()
1872 1873 1874 2028 2029 2030
2031 -def _clear_page_cache():
2032 """ 2033 Helper function to clear the page and block caches 2034 at most twice per page load. 2035 """ 2036 php.static(_menu_clear_page_cache, 'cache_cleared', 0) 2037 # Clear the page and block caches, but at most twice, including at 2038 # the end of the page load when there are multple links saved or deleted. 2039 if (_menu_clear_page_cache.cache_cleared == 0): 2040 cache_clear_all() 2041 # Keep track of which menus have expanded items. 2042 _menu_set_expanded_menus() 2043 _menu_clear_page_cache.cache_cleared = 1 2044 elif (_menu_clear_page_cache.cache_cleared == 1): 2045 register_shutdown_function('cache_clear_all') 2046 # Keep track of which menus have expanded items. 2047 register_shutdown_function('_menu_set_expanded_menus') 2048 _menu_clear_page_cache.cache_cleared = 2
2049 2050 2051
2052 -def _set_expanded_menus():
2053 """ 2054 Helper function to update a list of menus with expanded items 2055 """ 2056 names = [] 2057 result = db_query(\ 2058 "SELECT menu_name FROM {menu_links} " + \ 2059 "WHERE expanded != 0 GROUP BY menu_name") 2060 while True: 2061 n = db_fetch_array(result) 2062 if not n: 2063 break 2064 names.append( n['menu_name'] ) 2065 variable_set('menu_expanded', names)
2066 2067 2068
2069 -def _find_router_path(menu, link_path):
2070 """ 2071 Find the router path which will serve this path. 2072 2073 @param menu 2074 The full built menu. 2075 @param link_path 2076 The path for we are looking up its router path. 2077 @return 2078 A path from menu keys or empty if link_path points to a nonexisting 2079 place. 2080 """ 2081 parts = php.explode('/', link_path, MENU_MAX_PARTS) 2082 router_path = link_path 2083 if (not php.isset(menu, router_path)): 2084 ancestors = menu_get_ancestors(parts) 2085 ancestors.append('') 2086 for key,router_path in ancestors.items(): 2087 if (php.isset(menu, router_path)): 2088 break 2089 return router_path
2090 2091 2092 2132 2133 2134 2161 2162 2163 2222 2223 2224
2225 -def _update_parental_status(item, exclude = False):
2226 """ 2227 Check and update the has_children status for the parent of a link. 2228 """ 2229 # If plid == 0, there is nothing to update. 2230 if (item['plid']): 2231 # We may want to exclude the passed link as a possible child. 2232 where = (" AND mlid != %d" if exclude else '') 2233 # Check if at least one visible child exists in the table. 2234 parent_has_children = drupy_bool(db_result(db_query_range(\ 2235 "SELECT mlid FROM {menu_links} WHERE " + \ 2236 "menu_name = '%s' AND plid = %d AND hidden = 0" + where, \ 2237 item['menu_name'], item['plid'], item['mlid'], 0, 1))) 2238 db_query("UPDATE {menu_links} SET has_children = %d WHERE mlid = %d", \ 2239 parent_has_children, item['plid'])
2240 2241 2242 2261 2262 2263
2264 -def _router_build(callbacks):
2265 """ 2266 Helper function to build the router table based on the data from hook_menu. 2267 """ 2268 # First pass: separate callbacks from paths, making paths ready for 2269 # matching. Calculate fitness, and fill some default values. 2270 menu = [] 2271 for path,item in callbacks.items(): 2272 load_functions = [] 2273 to_arg_functions = [] 2274 fit = 0 2275 move = False 2276 parts = php.explode('/', path, MENU_MAX_PARTS) 2277 number_parts = php.count(parts) 2278 # We store the highest index of parts here to save some work in the fit 2279 # calculation loop. 2280 slashes = number_parts - 1 2281 # Extract load and to_arg functions. 2282 for k,part in parts.items(): 2283 match = False 2284 if (php.preg_match('/^%([a-z_]*)$/', part, matches)): 2285 if (php.empty(matches[1])): 2286 match = True 2287 load_functions[k] = None 2288 else: 2289 if (drupal_function_exists(matches[1] + '_to_arg')): 2290 to_arg_functions[k] = matches[1] + '_to_arg' 2291 load_functions[k] = None 2292 match = True 2293 if (drupal_function_exists(matches[1] + '_load')): 2294 function = matches[1] + '_load' 2295 # Create an array of arguments that will be passed to the _load 2296 # function when this menu path is checked, if 'load arguments' 2297 # exists. 2298 load_functions[k] = ({function : item['load arguments']} if \ 2299 php.isset(item, 'load arguments') else function) 2300 match = True 2301 if (match): 2302 parts[k] = '%' 2303 else: 2304 fit |= 1 << (slashes - k) 2305 if (fit): 2306 move = True 2307 else: 2308 # If there is no %, it fits maximally. 2309 fit = (1 << number_parts) - 1 2310 masks[fit] = 1 2311 item['load_functions'] = ('' if php.empty(load_functions) else \ 2312 php.serialize(load_functions)) 2313 item['to_arg_functions'] = ('' if php.empty(to_arg_functions) else \ 2314 php.serialize(to_arg_functions)) 2315 item += { 2316 'title': '', 2317 'weight': 0, 2318 'type': MENU_NORMAL_ITEM, 2319 '_number_parts': number_parts, 2320 '_parts': parts, 2321 '_fit': fit, 2322 } 2323 item += { 2324 '_visible' : drupy_bool((item['type'] & MENU_VISIBLE_IN_BREADCRUMB)), 2325 '_tab' : drupy_bool((item['type'] & MENU_IS_LOCAL_TASK)), 2326 } 2327 if (move): 2328 new_path = php.implode('/', item['_parts']) 2329 menu[new_path] = item 2330 sort[new_path] = number_parts 2331 else: 2332 menu[path] = item 2333 sort[path] = number_parts 2334 php.array_multisort(sort, SORT_NUMERIC, menu) 2335 if (menu): 2336 # Delete the existing router since we have some data to replace it. 2337 lib_database.query('DELETE FROM {menu_router}'); 2338 # Apply inheritance rules. 2339 for path,v in menu.items(): 2340 item = menu[path] 2341 if (not item['_tab']): 2342 # Non-tab items. 2343 item['tab_parent'] = '' 2344 item['tab_root'] = path 2345 for i in range(item['_number_parts'] - 1, 0, -1): 2346 parent_path = php.implode('/', php.array_slice(item['_parts'], 0, i)) 2347 if (php.isset(menu, parent_path)): 2348 parent = menu[parent_path] 2349 if (not php.isset(item, 'tab_parent')): 2350 # Parent stores the parent of the path. 2351 item['tab_parent'] = parent_path 2352 if (not php.isset(item, 'tab_root') and not parent['_tab']): 2353 item['tab_root'] = parent_path 2354 # If an access callback is not found for a default local task we use 2355 # the callback from the parent, since we expect them to be identical. 2356 # In all other cases, the access parameters must be specified. 2357 if ((item['type'] == MENU_DEFAULT_LOCAL_TASK) and \ 2358 not php.isset(item, 'access callback') and \ 2359 php.isset(parent, 'access callback')): 2360 item['access callback'] = parent['access callback'] 2361 if (not php.isset(item, 'access arguments') and \ 2362 php.isset(parent, 'access arguments')): 2363 item['access arguments'] = parent['access arguments'] 2364 # Same for page callbacks. 2365 if (not php.isset(item, 'page callback') and \ 2366 php.isset(parent, 'page callback')): 2367 item['page callback'] = parent['page callback'] 2368 if (not php.isset(item, 'page arguments') and \ 2369 php.isset(parent, 'page arguments')): 2370 item['page arguments'] = parent['page arguments'] 2371 if (not php.isset(item, 'access callback') and \ 2372 php.isset(item, 'access arguments')): 2373 # Default callback. 2374 item['access callback'] = 'user_access' 2375 if (not php.isset(item, 'access callback') or \ 2376 php.empty(item['page callback'])): 2377 item['access callback'] = 0 2378 if (is_bool(item['access callback'])): 2379 item['access callback'] = intval(item['access callback']) 2380 item += { 2381 'access arguments': {}, 2382 'access callback': '', 2383 'page arguments': {}, 2384 'page callback': '', 2385 'block callback': '', 2386 'title arguments': {}, 2387 'title callback': 't', 2388 'description': '', 2389 'position': '', 2390 'tab_parent': '', 2391 'tab_root': path, 2392 'path': path 2393 } 2394 title_arguments = (php.serialize(item['title arguments']) if \ 2395 item['title arguments'] else '') 2396 db_query( \ 2397 "INSERT INTO {menu_router} " + \ 2398 "(path, load_functions, to_arg_functions, access_callback, " + \ 2399 "access_arguments, page_callback, page_arguments, fit, " + \ 2400 "number_parts, tab_parent, tab_root, " + \ 2401 "title, title_callback, title_arguments, " + \ 2402 "type, block_callback, description, position, weight) " + \ 2403 "VALUES ('%s', '%s', '%s', '%s', " + \ 2404 "'%s', '%s', '%s', %d, " + \ 2405 "%d, '%s', '%s', " + \ 2406 "'%s', '%s', '%s', " + \ 2407 "%d, '%s', '%s', '%s', %d)", 2408 path, item['load_functions'], item['to_arg_functions'], \ 2409 item['access callback'], \ 2410 php.serialize(item['access arguments']), \ 2411 item['page callback'], php.serialize(item['page arguments']), \ 2412 item['_fit'], \ 2413 item['_number_parts'], item['tab_parent'], item['tab_root'], \ 2414 item['title'], item['title callback'], title_arguments, \ 2415 item['type'], item['block callback'], item['description'], \ 2416 item['position'], item['weight']) 2417 # Sort the masks so they are in order of descending fit, and store them. 2418 masks = php.array_keys(masks) 2419 rsort(masks) 2420 variable_set('menu_masks', masks) 2421 lib_cache.set('router:', menu, 'cache_menu'); 2422 return menu
2423 2424 2425 2426 # 2427 # Returns True if a path is external (e.g. http://example.com). 2428 #
2429 -def path_is_external(path):
2430 colonpos = php.strpos(path, ':') 2431 return (colonpos != False and not php.preg_match('not [/?#]not ', \ 2432 php.substr(path, 0, colonpos)) and \ 2433 filter_xss_bad_protocol(path, False) == check_plain(path))
2434 2435 2436
2437 -def _site_is_offline():
2438 """ 2439 Checks whether the site is off-line for maintenance. 2440 2441 This function will log the current user out and redirect to front page 2442 if the current user has no 'administer site configuration' permission. 2443 2444 @return 2445 False if the site is not off-line or its the login page or the user has 2446 'administer site configuration' permission. 2447 True for anonymous users not on the login page if the site is off-line. 2448 """ 2449 # Check if site is set to off-line mode. 2450 if (variable_get('site_offline', 0)): 2451 # Check if the user has administration privileges. 2452 if (user_access('administer site configuration')): 2453 # Ensure that the off-line message is displayed only once [allowing for 2454 # page redirects], and specifically suppress its display on the site 2455 # maintenance page. 2456 if (drupal_get_normal_path(php.GET['q']) != \ 2457 'admin/settings/site-maintenance'): 2458 drupal_set_message(t('Operating in off-line mode.'), 'status', False) 2459 else: 2460 # Anonymous users get a False at the login prompt, True otherwise. 2461 if (user_is_anonymous()): 2462 return php.GET['q'] != 'user' and php.GET['q'] != 'user/login' 2463 # Logged in users are unprivileged here, so they are logged out. 2464 php.require_once( drupal_get_path('plugin', 'user') + \ 2465 '/user.pages.inc' ) 2466 user_logout() 2467 return False
2468 2469 2470
2471 -def valid_path(form_item):
2472 """ 2473 Validates the path of a menu link being created or edited. 2474 2475 @return 2476 True if it is a valid path AND the current user has access permission, 2477 False otherwise. 2478 """ 2479 item = {} 2480 path = form_item['link_path'] 2481 # We indicate that a menu administrator is running the menu access check. 2482 lib_appglobals.menu_admin = True 2483 if (path == '<front>' or menu_path_is_external(path)): 2484 item = {'access': True} 2485 elif (php.preg_match('/\/\%/', path)): 2486 # Path is dynamic (ie 'user/%'), so check 2487 # directly against menu_router table. 2488 item = db_fetch_array(\ 2489 db_query("SELECT * FROM {menu_router} where path = '%s' ", path)) 2490 if (item): 2491 item['link_path'] = form_item['link_path'] 2492 item['link_title'] = form_item['link_title'] 2493 item['external'] = False 2494 item['options'] = '' 2495 _menu_link_translate(item) 2496 else: 2497 item = menu_get_item(path) 2498 lib_appglobals.menu_admin = False 2499 return item and item['access']
2500