Commit a243145d authored by Dries's avatar Dries

- Patch #137767 by chx and pwolanin: multiple menu support.

parent b4ef53ec
......@@ -781,8 +781,6 @@ function locale_translate_edit_form_submit($form_values, $form, &$form_state) {
// Refresh the locale cache.
locale_refresh_cache();
// Rebuild the menu, strings may have changed.
menu_rebuild();
$form_state['redirect'] = 'admin/build/translate/search';
return;
......
......@@ -38,7 +38,7 @@
* requested in the tree above, the callback for a/b would be used.
*
* The found callback function is called with any arguments specified
* in the "callback arguments" attribute of its menu item. The
* in the "page arguments" attribute of its menu item. The
* attribute must be an array. After these arguments, any remaining
* components of the path are appended as further arguments. In this
* way, the callback for a/b above could respond to a request for
......@@ -47,11 +47,10 @@
* For an illustration of this process, see page_example.module.
*
* Access to the callback functions is also protected by the menu system.
* The "access" attribute of each menu item is checked as the search for a
* callback proceeds. If this attribute is TRUE, then access is granted; if
* FALSE, then access is denied. The first found "access" attribute
* determines the accessibility of the target. Menu items may omit this
* attribute to use the value provided by an ancestor item.
* The "access callback" with an optional "access arguments" of each menu
* item is called before the page callback proceeds. If this returns TRUE,
* then access is granted; if FALSE, then access is denied. Menu items may
* omit this attribute to use the value provided by an ancestor item.
*
* In the default Drupal interface, you will notice many links rendered as
* tabs. These are known in the menu system as "local tasks", and they are
......@@ -65,6 +64,11 @@
* links not to its provided path, but to its parent item's path instead.
* The default task's path is only used to place it appropriately in the
* menu hierarchy.
*
* Everything described so far is stored in the menu_router table. The
* menu_links table holds the visible menu links. By default these are
* derived from the same hook_menu definitons, however you are free to
* add more with menu_link_save().
*/
/**
......@@ -126,18 +130,6 @@
*/
define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT);
/**
* Custom items are those defined by the administrator. Reserved for internal
* use; do not return from hook_menu() implementations.
*/
define('MENU_CUSTOM_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB | MENU_CREATED_BY_ADMIN | MENU_MODIFIABLE_BY_ADMIN);
/**
* Custom menus are those defined by the administrator. Reserved for internal
* use; do not return from hook_menu() implementations.
*/
define('MENU_CUSTOM_MENU', MENU_IS_ROOT | MENU_VISIBLE_IN_TREE | MENU_CREATED_BY_ADMIN | MENU_MODIFIABLE_BY_ADMIN);
/**
* @} End of "Menu item types".
*/
......@@ -157,37 +149,31 @@
* @} End of "Menu status codes".
*/
/**
* @Name Menu operations
* @{
* Menu helper possible operations.
*/
define('MENU_HANDLE_REQUEST', 0);
define('MENU_RENDER_LINK', 1);
/**
* @} End of "Menu operations."
*/
/**
* @Name Menu alterations
* @Name Menu tree parameters
* @{
* Menu alter phases
* Menu tree
*/
/**
* Alter the menu as defined in modules, keys are like user/%user.
/**
* The maximum number of path elements for a menu callback
*/
define('MENU_ALTER_MODULE_DEFINED', 0);
define('MENU_MAX_PARTS', 6);
/**
* Alter the menu after the first preprocessing phase, keys are like user/%.
* The maximum depth of a menu links tree - matches the number of p columns.
*/
define('MENU_ALTER_PREPROCESSED', 1);
define('MENU_MAX_DEPTH', 6);
/**
* @} End of "Menu alterations".
* @} End of "Menu tree parameters".
*/
/**
......@@ -255,11 +241,11 @@ function menu_get_ancestors($parts) {
* necessary change.
*
* Integer values are mapped according to the $map parameter. For
* example, if unserialize($data) is array('node_load', 1) and $map is
* array('node', '12345') then 'node_load' will not be changed
* because it is not an integer, but 1 will as it is an integer. As
* $map[1] is '12345', 1 will be replaced with '12345'. So the result
* will be array('node_load', '12345').
* example, if unserialize($data) is array('view', 1) and $map is
* array('node', '12345') then 'view' will not be changed because
* it is not an integer, but 1 will as it is an integer. As $map[1]
* is '12345', 1 will be replaced with '12345'. So the result will
* be array('node_load', '12345').
*
* @param @data
* A serialized array.
......@@ -283,50 +269,27 @@ function menu_unserialize($data, $map) {
}
/**
* Replaces the statically cached item for a given path.
* Get the menu callback for the a path.
*
* @param $path
* The path
* @param $item
* The menu item. This is a menu entry, an associative array,
* with keys like title, access callback, access arguments etc.
* A path, or NULL for the current path
*/
function menu_set_item($path, $item) {
menu_get_item($path, $item);
}
function menu_get_item($path = NULL, $item = NULL) {
function menu_get_item($path = NULL) {
static $items;
if (!isset($path)) {
$path = $_GET['q'];
}
if (isset($item)) {
$items[$path] = $item;
}
if (!isset($items[$path])) {
$original_map = arg(NULL, $path);
$parts = array_slice($original_map, 0, 6);
$parts = array_slice($original_map, 0, MENU_MAX_PARTS);
list($ancestors, $placeholders) = menu_get_ancestors($parts);
$item->active_trail = array();
if ($item = db_fetch_object(db_query_range('SELECT * FROM {menu} WHERE path IN ('. implode (',', $placeholders) .') ORDER BY fit DESC', $ancestors, 0, 1))) {
// We need to access check the parents to match the navigation tree
// behaviour. The last parent is always the item itself.
$args = explode(',', $item->parents);
$placeholders = implode(', ', array_fill(0, count($args), '%d'));
$result = db_query('SELECT * FROM {menu} WHERE mid IN ('. $placeholders .') ORDER BY mleft', $args);
$item->access = TRUE;
while ($item->access && ($parent = db_fetch_object($result))) {
$map = _menu_translate($parent, $original_map);
if ($map === FALSE) {
$items[$path] = FALSE;
return FALSE;
}
if ($parent->access) {
$item->active_trail[] = $parent;
}
else {
$item->access = FALSE;
}
if ($item = db_fetch_object(db_query_range('SELECT * FROM {menu_router} WHERE path IN ('. implode (',', $placeholders) .') ORDER BY fit DESC', $ancestors, 0, 1))) {
$map = _menu_translate($item, $original_map);
if ($map === FALSE) {
$items[$path] = FALSE;
return FALSE;
}
if ($item->access) {
$item->map = $map;
......@@ -335,11 +298,11 @@ function menu_get_item($path = NULL, $item = NULL) {
}
$items[$path] = $item;
}
return $items[$path];
return drupal_clone($items[$path]);
}
/**
* Execute the handler associated with the active menu item.
* Execute the page callback associated with the current path
*/
function menu_execute_active_handler() {
if ($item = menu_get_item()) {
......@@ -349,77 +312,47 @@ function menu_execute_active_handler() {
}
/**
* Handles dynamic path translation, title and description translation and
* menu access control.
*
* When a user arrives on a page such as node/5, this function determines
* what "5" corresponds to, by inspecting the page's menu path definition,
* node/%node. This will call node_load(5) to load the corresponding node
* object.
*
* It also works in reverse, to allow the display of tabs and menu items which
* contain these dynamic arguments, translating node/%node to node/5.
* This operation is called MENU_RENDER_LINK.
*
* Translation of menu item titles and descriptions are done here to
* allow for storage of English strings in the database, and be able to
* generate menus in the language required to generate the current page.
* Loads objects into the map as defined in the $item->load_functions.
*
* @param $item
* A menu item object
* @param $map
* An array of path arguments (ex: array('node', '5'))
* @param $operation
* The path translation operation to perform:
* - MENU_HANDLE_REQUEST: An incoming page reqest; map with appropriate callback.
* - MENU_RENDER_LINK: Render an internal path as a link.
* @return
* Returns the map with objects loaded as defined in the
* $item->load_functions. Also, $item->link_path becomes the path ready
* for printing, aliased. $item->alias becomes TRUE to mark this, so you can
* just pass (array)$item to l() as the third parameter.
* $item->access becomes TRUE if the item is accessible, FALSE otherwise.
*/
function _menu_translate(&$item, $map, $operation = MENU_HANDLE_REQUEST) {
// Check if there are dynamic arguments in the path that need to be calculated.
// If there are to_arg_functions, then load_functions is also not empty
// because it was built so in menu_rebuild. Therefore, it's enough to test
// load_functions.
* Returns TRUE for success, FALSE if an object cannot be loaded
*/
function _menu_load_objects($item, &$map) {
if ($item->load_functions) {
$load_functions = unserialize($item->load_functions);
$to_arg_functions = unserialize($item->to_arg_functions);
$path_map = ($operation == MENU_HANDLE_REQUEST) ? $map : explode('/', $item->path);
foreach ($load_functions as $index => $load_function) {
// Translate place-holders into real values.
if ($operation == MENU_RENDER_LINK) {
if (isset($to_arg_functions[$index])) {
$to_arg_function = $to_arg_functions[$index];
$return = $to_arg_function(!empty($map[$index]) ? $map[$index] : '');
if (!empty($map[$index]) || isset($return)) {
$path_map[$index] = $return;
}
else {
unset($path_map[$index]);
}
}
else {
$path_map[$index] = isset($map[$index]) ? $map[$index] : '';
}
}
// We now have a real path regardless of operation, map it.
if ($load_function) {
$return = $load_function(isset($path_map[$index]) ? $path_map[$index] : '');
$path_map = $map;
foreach ($load_functions as $index => $function) {
if ($function) {
$return = $function(isset($path_map[$index]) ? $path_map[$index] : '');
// If callback returned an error or there is no callback, trigger 404.
if ($return === FALSE) {
$item->access = FALSE;
$map = FALSE;
return FALSE;
}
$map[$index] = $return;
}
}
// Re-join the path with the new replacement value and alias it.
$item->link_path = drupal_get_path_alias(implode('/', $path_map));
}
return TRUE;
}
/**
* Check access to a menu item using the access callback
*
* @param $item
* A menu item object
* @param $map
* An array of path arguments (ex: array('node', '5'))
* @return
* $item->access becomes TRUE if the item is accessible, FALSE otherwise.
*/
function _menu_check_access(&$item, $map) {
// Determine access callback, which will decide whether or not the current user has
// access to this path.
$callback = $item->access_callback;
......@@ -438,11 +371,12 @@ function _menu_translate(&$item, $map, $operation = MENU_HANDLE_REQUEST) {
$item->access = call_user_func_array($callback, $arguments);
}
}
$item->alias = TRUE;
}
function _menu_item_localize(&$item) {
// Translate the title to allow storage of English title strings
// in the database, yet be able to display them in the language
// required to generate the page in.
// in the database, yet display of them in the language required
// by the current user.
$callback = $item->title_callback;
// t() is a special case. Since it is used very close to all the time,
// we handle it directly instead of using indirect, slower methods.
......@@ -467,31 +401,247 @@ function _menu_translate(&$item, $map, $operation = MENU_HANDLE_REQUEST) {
if (!empty($item->description)) {
$item->description = t($item->description);
}
}
/**
* Handles dynamic path translation and menu access control.
*
* When a user arrives on a page such as node/5, this function determines
* what "5" corresponds to, by inspecting the page's menu path definition,
* node/%node. This will call node_load(5) to load the corresponding node
* object.
*
* It also works in reverse, to allow the display of tabs and menu items which
* contain these dynamic arguments, translating node/%node to node/5.
*
* Translation of menu item titles and descriptions are done here to
* allow for storage of English strings in the database, and translation
* to the language required to generate the current page
*
* @param $item
* A menu item object
* @param $map
* An array of path arguments (ex: array('node', '5'))
* @param $to_arg
* Execute $item->to_arg_functions or not. Use only if you want to render a
* path from the menu table, for example tabs.
* @return
* Returns the map with objects loaded as defined in the
* $item->load_functions. $item->access becomes TRUE if the item is
* accessible, FALSE otherwise. $item->href is set according to the map.
* If an error occurs during calling the load_functions (like trying to load
* a non existing node) then this function return FALSE.
*/
function _menu_translate(&$item, $map, $to_arg = FALSE) {
$path_map = $map;
if (!_menu_load_objects($item, $map)) {
// An error occurred loading an object.
$item->access = FALSE;
return FALSE;
}
if ($to_arg) {
_menu_link_map_translate($path_map, $item->to_arg_functions);
}
// Generate the link path for the page request or local tasks.
$link_map = explode('/', $item->path);
for ($i = 0; $i < $item->number_parts; $i++) {
if ($link_map[$i] == '%') {
$link_map[$i] = $path_map[$i];
}
}
$item->href = implode('/', $link_map);
_menu_check_access($item, $map);
_menu_item_localize($item);
return $map;
}
/**
* This function translates the path elements in the map using any to_arg
* helper function. These functions take an argument and return an object.
* See http://drupal.org/node/109153 for more information.
*
* @param map
* An array of path arguments (ex: array('node', '5'))
* @param $to_arg_functions
* An array of helper function (ex: array(1 => 'node_load'))
*/
function _menu_link_map_translate(&$map, $to_arg_functions) {
if ($to_arg_functions) {
$to_arg_functions = unserialize($to_arg_functions);
foreach ($to_arg_functions as $index => $function) {
// Translate place-holders into real values.
$arg = $function(!empty($map[$index]) ? $map[$index] : '');
if (!empty($map[$index]) || isset($arg)) {
$map[$index] = $arg;
}
else {
unset($map[$index]);
}
}
}
}
/**
* This function is similar to _menu_translate() but does link-specific
* preparation such as always calling to_arg functions
*
* @param $item
* A menu item object
* @return
* Returns the map of path arguments with objects loaded as defined in the
* $item->load_functions.
* $item->access becomes TRUE if the item is accessible, FALSE otherwise.
* $item->href is altered if there is a to_arg function.
*/
function _menu_link_translate(&$item) {
if ($item->external) {
$item->access = 1;
$map = array();
}
else {
$map = explode('/', $item->href);
_menu_link_map_translate($map, $item->to_arg_functions);
$item->href = implode('/', $map);
// Note- skip callbacks without real values for their arguments
if (strpos($item->href, '%') !== FALSE) {
$item->access = FALSE;
return FALSE;
}
if (!_menu_load_objects($item, $map)) {
// An error occured loading an object
$item->access = FALSE;
return FALSE;
}
// TODO: menu_tree may set this ahead of time for links to nodes
if (!isset($item->access)) {
_menu_check_access($item, $map);
}
// If the link title matches that of a router item, localize it.
if (isset($item->title) && ($item->title == $item->link_title)) {
_menu_item_localize($item);
$item->link_title = $item->title;
}
}
$item->options = unserialize($item->options);
return $map;
}
/**
* Returns a rendered menu tree. The tree is expanded based on the current
* path and dynamic paths are also changed according to the defined to_arg
* functions (for example the 'My account' link is changed from user/% to
* a link with the current user's uid).
*
* @param $menu_name
* The name of the menu.
* @return
* The rendered HTML of that menu on the current page.
*/
function menu_tree($menu_name = 'navigation') {
static $menu_output = array();
if (!isset($menu_output[$menu_name])) {
$tree = menu_tree_data($menu_name);
$menu_output[$menu_name] = menu_tree_output($tree);
}
return $menu_output[$menu_name];
}
/**
* Returns a rendered menu tree.
*
* @param $tree
* A data structure representing the tree as returned from menu_tree_data.
* @return
* The rendered HTML of that data structure.
*/
function menu_tree() {
if ($item = menu_get_item()) {
if ($item->access) {
$args = explode(',', $item->parents);
$placeholders = implode(', ', array_fill(0, count($args), '%d'));
function menu_tree_output($tree) {
$output = '';
foreach ($tree as $data) {
if (!$data['link']->hidden) {
$link = theme('menu_item_link', $data['link']);
if ($data['below']) {
$output .= theme('menu_item', $link, $data['link']->has_children, menu_tree_output($data['below']));
}
else {
$output .= theme('menu_item', $link, $data['link']->has_children);
}
}
// Show the root menu for access denied.
else {
$args = 0;
$placeholders = '%d';
}
return $output ? theme('menu_tree', $output) : '';
}
/**
* Get the data structure representing a named menu tree, based on the current
* page. The tree order is maintained by storing each parent in an invidual
* field, see http://drupal.org/node/141866 for more.
*
* @param $menu_name
* The named menu links to return
* @return
* An array of menu links, in the order they should be rendered. The array
* is a list of associative arrays -- these have two keys, link and below.
* link is a menu item, ready for theming as a link. Below represents the
* submenu below the link if there is one and it is a similar list that was
* described so far.
*/
function menu_tree_data($menu_name = 'navigation') {
static $tree = array();
if ($item = menu_get_item()) {
if (!isset($tree[$menu_name])) {
if ($item->access) {
$parents = db_fetch_array(db_query("SELECT p1, p2, p3, p4, p5 FROM {menu_links} WHERE menu_name = '%s' AND href = '%s'", $menu_name, $item->href));
// We may be on a local task that's not in the links
// TODO how do we handle the case like a local task on a specific node in the menu?
if (empty($parents)) {
$parents = db_fetch_array(db_query("SELECT p1, p2, p3, p4, p5 FROM {menu_links} WHERE menu_name = '%s' AND href = '%s'", $menu_name, $item->tab_root));
}
$parents[] = '0';
$args = $parents = array_unique($parents);
$placeholders = implode(', ', array_fill(0, count($args), '%d'));
$expanded = variable_get('menu_expanded', array());
if (in_array($menu_name, $expanded)) {
do {
$result = db_query("SELECT mlid FROM {menu_links} WHERE expanded != 0 AND AND has_children != 0 AND menu_name = '%s' AND plid IN (". $placeholders .') AND mlid NOT IN ('. $placeholders .')', array_merge(array($menu_name), $args, $args));
while ($item = db_fetch_array($result)) {
$args[] = $item['mlid'];
}
$placeholders = implode(', ', array_fill(0, count($args), '%d'));
} while (db_num_rows($result));
}
array_unshift($args, $menu_name);
}
// Show the root menu for access denied.
else {
$args = array('navigation', '0');
$placeholders = '%d';
}
// LEFT JOIN since there is no match in {menu_router} for an external link.
// No need to order by p6 - there is a sort by weight later.
list(, $tree[$menu_name]) = _menu_tree_data(db_query("
SELECT *, ml.weight + 50000 AS weight FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
WHERE ml.menu_name = '%s' AND ml.plid IN (". $placeholders .")
ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC", $args), $parents);
// TODO: cache_set() for the untranslated links
// TODO: special case node links and access check via db_rewite_sql()
// TODO: access check / _menu_link_translate on each here
}
list(, $menu) = _menu_tree(db_query('SELECT * FROM {menu} WHERE pid IN ('. $placeholders .') AND visible = 1 ORDER BY mleft', $args));
return $menu;
return $tree[$menu_name];
}
}
/**
* Renders a menu tree from a database result resource.
* Build the data representing a menu tree.
*
* The function is a bit complex because the rendering of an item depends on
* the next menu item. So we are always rendering the element previously
......@@ -499,73 +649,82 @@ function menu_tree() {
*
* @param $result
* The database result.
* @param $parents
* An array of the plid values that represent the path from the current page
* to the root of the menu tree.
* @param $depth
* The depth of the current menu tree.
* @param $link
* The first link in the current menu tree.
* @param $has_children
* Whether the first link has children.
* @param $previous_element
* The previous menu link in the current menu tree.
* @return
* A list, the first element is the first item after the submenu, the second
* is the rendered HTML of the children.
* See menu_tree_data for a description of the data structure.
*/
function _menu_tree($result = NULL, $depth = 0, $link = '', $has_children = FALSE) {
static $map;
function _menu_tree_data($result = NULL, $parents = array(), $depth = 1, $previous_element = '') {
$remnant = NULL;
$tree = '';
// Fetch the current path and cache it.
if (!isset($map)) {
$map = arg(NULL);
}
$tree = array();
while ($item = db_fetch_object($result)) {
// Access check and handle dynamic path translation.
_menu_translate($item, $map, MENU_RENDER_LINK);
// TODO - move this to the parent function, so the untranslated link data
// can be cached.
_menu_link_translate($item);
if (!$item->access) {
continue;
}
if ($item->attributes) {
$item->attributes = unserialize($item->attributes);
}
// We need to determine if we're on the path to root so we can later build
// the correct active trail and breadcrumb.
$item->path_to_root = in_array($item->mlid, $parents);
// The weights are uniform 5 digits because of the 50000 offset in the
// query. We add mlid at the end of the index to insure uniqueness.
$index = $previous_element ? ($previous_element->weight .' '. $previous_element->title . $previous_element->mlid) : '';
// The current item is the first in a new submenu.
if ($item->depth > $depth) {
// _menu_tree returns an item and the HTML of the rendered menu tree.
list($item, $menu) = _menu_tree($result, $item->depth, theme('menu_item_link', $item), $item->has_children);
// Theme the menu.
$menu = $menu ? theme('menu_tree', $menu) : '';
// $link is the previous element.
$tree .= $link ? theme('menu_item', $link, $has_children, $menu) : $menu;
// _menu_tree returns an item and the menu tree structure.
list($item, $below) = _menu_tree_data($result, $parents, $item->depth, $item);
$tree[$index] = array(
'link' => $previous_element,
'below' => $below,
);
// We need to fall back one level.
if ($item->depth < $depth) {
ksort($tree);
return array($item, $tree);
}
// This will be the link to be output in the next iteration.
$link = $item ? theme('menu_item_link', $item) : '';
$has_children = $item ? $item->has_children : FALSE;
$previous_element = $item;
}
// We are in the same menu. We render the previous element.
// We are in the same menu. We render the previous element, $previous_element.
elseif ($item->depth == $depth) {