=== modified file 'includes/menu.inc' --- includes/menu.inc 2007-04-30 17:03:22 +0000 +++ includes/menu.inc 2007-05-06 03:49:57 +0000 @@ -283,50 +283,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); 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; - } + + $map = _menu_translate($item, $original_map); + if ($map === FALSE) { + $items[$path] = FALSE; + return FALSE; } if ($item->access) { $item->map = $map; @@ -339,7 +316,7 @@ function menu_get_item($path = NULL, $it } /** - * 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 +326,48 @@ 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 the 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,7 +386,53 @@ function _menu_translate(&$item, $map, $ $item->access = call_user_func_array($callback, $arguments); } } - $item->alias = TRUE; +} + +/** + * 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. + * 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 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')) + * @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->link_path is set. + */ + +function _menu_translate(&$item, $map) { + + $path_map = $map; + if (!_menu_load_objects($item, $map)) { + // An error occured loading an object + $item->access = FALSE; + return FALSE; + } + + // 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->link_path = implode('/', $link_map); + _menu_check_access($item, $map); // Translate the title to allow storage of English title strings // in the database, yet be able to display them in the language @@ -472,26 +466,125 @@ function _menu_translate(&$item, $map, $ } /** + * When rendering a menu link, this function is similar to + * _menu_translate() but does other link-specific preparation. + * + * @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->link_path is altered if there is a to_arg function. + */ +function _menu_link_translate(&$item) { + if ($item->external) { + $item->access = 1; + } + else { + $map = explode('/', $item->link_path); + if ($item->to_arg_functions) { + $to_arg_functions = unserialize($item->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]); + } + } + // Replace the link path using new values + $item->link_path = implode('/', $map); + } + 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); + } + } + $item->options = unserialize($item->options); + + return $map; +} + +/** * Returns a rendered menu tree. */ -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($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]; +} + +function _menu_tree_output($tree) { + $output = ''; + + foreach ($tree as $data) { + $link = theme('menu_item_link', $data['link']); + if ($data['below']) { + $output .= theme('menu_item', $link, $data['link']->has_children, _menu_tree_output($data['below'])); } - // Show the root menu for access denied. else { - $args = 0; - $placeholders = '%d'; + $output .= theme('menu_item', $link, $data['link']->has_children); } - list(, $menu) = _menu_tree(db_query('SELECT * FROM {menu} WHERE pid IN ('. $placeholders .') AND visible = 1 ORDER BY mleft', $args)); - return $menu; } + return $output ? theme('menu_tree', $output) : ''; } /** - * Renders a menu tree from a database result resource. + * Get the data representing a named menu tree + * + * @param $menu_name + * The named menu links to return + * @return + * An array of menu links, in the order they should be rendered. + */ + +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_result(db_query("SELECT parents FROM {menu_links} WHERE menu_name = '%s' AND link_path = '%s'", $menu_name, $item->link_path)); + // We may be on a local task that's not in the links + // TODO how do we hadle the case like a local task on a specific node in the menu? + if (empty($parents) && $item->tab_parent) { + $parents = db_result(db_query("SELECT parents FROM {menu_links} WHERE menu_name = '%s' AND link_path = '%s'", $menu_name, $item->tab_root)); + } + if (empty($parents)) { + $parents = '0'; + } + $args = explode(',', $parents); + $placeholders = implode(', ', array_fill(0, count($args), '%d')); + $placeholders = "'%s', ". $placeholders; + array_unshift($args, $menu_name); + } + // Show the root menu for access denied. + else { + $args = array('navigation', 0); + $placeholders = '%d'; + } + list(, $tree[$menu_name]) = _menu_tree_data(db_query("SELECT * FROM {menu_links} ml LEFT JOIN {menu} m ON m.path = ml.path WHERE ml.menu_name = '%s' AND ml.plid IN (". $placeholders .") AND disabled = 0 ORDER BY mleft", $args)); + } + return $tree[$menu_name]; + } +} + + +/** + * 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 @@ -501,50 +594,43 @@ function menu_tree() { * The database result. * @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. + * is the data of the child menu items. */ -function _menu_tree($result = NULL, $depth = 0, $link = '', $has_children = FALSE) { - static $map; + +function _menu_tree_data($result = NULL, $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); + _menu_link_translate($item); if (!$item->access) { continue; } - if ($item->attributes) { - $item->attributes = unserialize($item->attributes); - } // 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, $item->depth, $item); + $tree[] = array( + 'link' => $previous_element, + 'below' => $below, + ); // 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) { - // $link is the previous element. - $tree .= theme('menu_item', $link, $has_children); + if (!empty($previous_element)) { // Only the first time + $tree[] = array( + 'link' => $previous_element, + 'below' => '', + ); + } // This will be the link to be output in the next iteration. - $link = theme('menu_item_link', $item); - $has_children = $item->has_children; + $previous_element = $item; } // The submenu ended with the previous item, we need to pass back the // current element. @@ -553,19 +639,22 @@ function _menu_tree($result = NULL, $dep break; } } - if ($link) { + if ($previous_element) { // We have one more link dangling. - $tree .= theme('menu_item', $link, $has_children); + $tree[] = array( + 'link' => $previous_element, + 'below' => '', + ); } return array($remnant, $tree); } + /** * Generate the HTML output for a single menu link. */ -function theme_menu_item_link($item) { - $link = (array)$item; - return l($link['title'], $link['link_path'], $link); +function theme_menu_item_link($link) { + return l($link->link_text, $link->link_path, $link->options); } /** @@ -616,316 +705,40 @@ function menu_get_active_help() { return $output; } -function menu_path_is_external($path) { - $colonpos = strpos($path, ':'); - return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && filter_xss_bad_protocol($path, FALSE) == check_plain($path); -} - /** - * Populate the database representation of the menu. + * Build a list of named menus. */ -function menu_rebuild() { - // TODO: split menu and menu links storage. - $menu = module_invoke_all('menu'); - - // Alter the menu as defined in modules, keys are like user/%user. - drupal_alter('menu', $menu, MENU_ALTER_MODULE_DEFINED); - db_query('DELETE FROM {menu}'); - $mid = 1; - - // First pass: separate callbacks from pathes, making pathes ready for - // matching. Calculate fitness, and fill some default values. - foreach ($menu as $path => $item) { - $load_functions = array(); - $to_arg_functions = array(); - $fit = 0; - $move = FALSE; - if (!isset($item['_external'])) { - $item['_external'] = menu_path_is_external($path); - } - if ($item['_external']) { - $number_parts = 0; - $parts = array(); - } - else { - $parts = explode('/', $path, 6); - $number_parts = count($parts); - // We store the highest index of parts here to save some work in the fit - // calculation loop. - $slashes = $number_parts - 1; - // extract functions - foreach ($parts as $k => $part) { - $match = FALSE; - if (preg_match('/^%([a-z_]*)$/', $part, $matches)) { - if (empty($matches[1])) { - $match = TRUE; - $load_functions[$k] = NULL; - } - else { - if (function_exists($matches[1] .'_to_arg')) { - $to_arg_functions[$k] = $matches[1] .'_to_arg'; - $load_functions[$k] = NULL; - $match = TRUE; - } - if (function_exists($matches[1] .'_load')) { - $load_functions[$k] = $matches[1] .'_load'; - $match = TRUE; - } - } - } - if ($match) { - $parts[$k] = '%'; - } - else { - $fit |= 1 << ($slashes - $k); - } - } - if ($fit) { - $move = TRUE; - } - else { - // If there is no %, it fits maximally. - $fit = (1 << $number_parts) - 1; - } - } - $item['load_functions'] = empty($load_functions) ? '' : serialize($load_functions); - $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions); - $item += array( - 'title' => '', - 'weight' => 0, - 'type' => MENU_NORMAL_ITEM, - '_number_parts' => $number_parts, - '_parts' => $parts, - '_fit' => $fit, - '_mid' => $mid++, - '_children' => array(), +function menu_get_names($reset = FALSE) { + static $names; + // TODO - use cache system to save this + + if ($reset || empty($names)) { + $names = module_invoke_all('menu_info'); + $names['navigation'] = array( + 'customizable' => TRUE, + 'title' => t('Navigation'), + 'title callback' => 'check_plain', + 'callback arguments' => array('$user->name'), + 'description' => t('Main navigation menu'), // What's the existing t() string? ); - $item += array( - '_visible' => (bool)($item['type'] & MENU_VISIBLE_IN_TREE), - '_tab' => (bool)($item['type'] & MENU_IS_LOCAL_TASK), - ); - if ($move) { - $new_path = implode('/', $item['_parts']); - unset($menu[$path]); - } - else { - $new_path = $path; - } - $menu_path_map[$path] = $new_path; - $menu[$new_path] = $item; - } - - // Alter the menu after the first preprocessing phase, keys are like user/%. - drupal_alter('menu', $menu, MENU_ALTER_PREPROCESSED); - $menu_path_map[''] = ''; - // Second pass: prepare for sorting and find parents. - foreach ($menu as $path => $item) { - $item = &$menu[$path]; - $parent_path = $path; - $parents = array($item['_mid']); - $depth = 1; - if (isset($item['parent']) && isset($menu_path_map[$item['parent']])) { - $item['parent'] = $menu_path_map[$item['parent']]; - } - if ($item['_visible'] || $item['_tab']) { - while ($parent_path) { - if (isset($menu[$parent_path]['parent'])) { - if (isset($menu_path_map[$menu[$parent_path]['parent']])) { - $menu[$parent_path]['parent'] = $menu_path_map[$menu[$parent_path]['parent']]; - } - $parent_path = $menu[$parent_path]['parent']; - } - else { - $parent_path = substr($parent_path, 0, strrpos($parent_path, '/')); - } - if (isset($menu[$parent_path]) && $menu[$parent_path]['_visible']) { - $parent = $menu[$parent_path]; - $parents[] = $parent['_mid']; - $depth++; - if (!isset($item['_pid'])) { - $item['_pid'] = $parent['_mid']; - $item['_visible_parent_path'] = $parent_path; - } - } + foreach ($names as $key => $data) { + if (!isset($data['customizable'])) { + $names[$key]['customizable'] = TRUE; } - } - $parents[] = 0; - $parents = implode(',', array_reverse($parents)); - // Store variables and set defaults. - $item += array( - '_pid' => 0, - '_depth' => ($item['_visible'] ? $depth : $item['_number_parts']), - '_parents' => $parents, - '_slashes' => $slashes, - ); - // This sorting works correctly only with positive numbers, - // so we shift negative weights to be positive. - $sort[$path] = $item['_depth'] . sprintf('%05d', $item['weight'] + 50000) . $item['title']; - unset($item); - } - array_multisort($sort, $menu); - - // We are now sorted, so let's build the tree. - $children = array(); - foreach ($menu as $path => $item) { - if (!empty($item['_pid'])) { - $menu[$item['_visible_parent_path']]['_children'][] = $path; - } - } - menu_renumber($menu); - - // Apply inheritance rules. - foreach ($menu as $path => $item) { - if ($item['_external']) { - $item['access callback'] = 1; - } - else { - $item = &$menu[$path]; - for ($i = $item['_number_parts'] - 1; $i; $i--) { - $parent_path = implode('/', array_slice($item['_parts'], 0, $i)); - if (isset($menu[$parent_path])) { - $parent = $menu[$parent_path]; - // If a callback is not found, we try to find the first parent that - // has this callback. When found, its callback argument will also be - // copied but only if there is none in the current item. - - // Because access is checked for each visible parent as well, we only - // inherit if arguments were given without a callback. Otherwise the - // inherited check would be identical to that of the parent. We do - // not inherit from visible parents which are themselves inherited. - if (!isset($item['access callback']) && isset($parent['access callback']) && !(isset($parent['access inherited']) && $parent['_visible'])) { - if (isset($item['access arguments'])) { - $item['access callback'] = $parent['access callback']; - } - else { - $item['access callback'] = 1; - // If a children of this element has an argument, we need to pair - // that with a real callback, not the 1 we set above. - $item['access inherited'] = TRUE; - } - } - - // Unlike access callbacks, there are no shortcuts for page callbacks. - if (!isset($item['page callback']) && isset($parent['page callback'])) { - $item['page callback'] = $parent['page callback']; - if (!isset($item['page arguments']) && isset($parent['page arguments'])) { - $item['page arguments'] = $parent['page arguments']; - } - } - } - } - if (!isset($item['access callback'])) { - $item['access callback'] = isset($item['access arguments']) ? 'user_access' : 0; - } - if (is_bool($item['access callback'])) { - $item['access callback'] = intval($item['access callback']); + if (!isset($data['title callback'])) { + $names[$key]['title callback'] = FALSE; } - if (empty($item['page callback'])) { - $item['access callback'] = 0; - } - } - - if ($item['_tab']) { - if (isset($item['parent'])) { - $item['_depth'] = $item['parent'] ? $menu[$item['parent']]['_depth'] + 1 : 1; + if (!isset($data['callback arguments'])) { + $names[$key]['callback arguments'] = array(); } - else { - $item['parent'] = implode('/', array_slice($item['_parts'], 0, $item['_number_parts'] - 1)); - } - } - else { - // Non-tab items specified the parent for visible links, and it's - // stored in parents, parent stores the tab parent. - $item['parent'] = $path; - } - - $insert_item = $item; - unset($item); - $item = $insert_item + array( - 'access arguments' => array(), - 'access callback' => '', - 'page arguments' => array(), - 'page callback' => '', - '_mleft' => 0, - '_mright' => 0, - 'block callback' => '', - 'title arguments' => array(), - 'title callback' => 't', - 'description' => '', - 'position' => '', - 'attributes' => '', - 'query' => '', - 'fragment' => '', - 'absolute' => '', - 'html' => '', - ); - $link_path = $item['to_arg_functions'] ? $path : drupal_get_path_alias($path); - - $item['title arguments'] = empty($item['title arguments']) ? '' : serialize($item['title arguments']); - - if ($item['attributes']) { - $item['attributes'] = serialize($item['attributes']); - } - - // Check for children that are visible in the menu - $has_children = FALSE; - foreach ($item['_children'] as $child) { - if ($menu[$child]['_visible']) { - $has_children = TRUE; - break; - } - } - // We remove disabled items here -- this way they will be numbered in the - // tree so the menu overview screen can show them. - if (!empty($item['disabled'])) { - $item['_visible'] = FALSE; - } - db_query("INSERT INTO {menu} ( - mid, pid, path, load_functions, to_arg_functions, - access_callback, access_arguments, page_callback, page_arguments, - title_callback, title_arguments, fit, number_parts, visible, - parents, depth, has_children, tab, title, parent, - type, mleft, mright, block_callback, description, position, - link_path, attributes, query, fragment, absolute, html) - VALUES (%d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', - %d, %d, %d, '%s', %d, %d, %d, '%s', '%s', '%s', %d, %d, '%s', '%s', - '%s', '%s', '%s', '%s', '%s', %d, %d)", - $item['_mid'], $item['_pid'], $path, $item['load_functions'], - $item['to_arg_functions'], $item['access callback'], - serialize($item['access arguments']), $item['page callback'], - serialize($item['page arguments']), $item['title callback'], - $item['title arguments'], $item['_fit'], - $item['_number_parts'], $item['_visible'], $item['_parents'], - $item['_depth'], $has_children, $item['_tab'], - $item['title'], $item['parent'], $item['type'], $item['_mleft'], - $item['_mright'], $item['block callback'], $item['description'], - $item['position'], $link_path, - $item['attributes'], $item['query'], $item['fragment'], - $item['absolute'], $item['html']); - } -} - -function menu_renumber(&$tree) { - foreach ($tree as $key => $element) { - if (!isset($tree[$key]['_mleft'])) { - _menu_renumber($tree, $key); } } + return $names; } -function _menu_renumber(&$tree, $key) { - static $counter = 1; - if (!isset($tree[$key]['_mleft'])) { - $tree[$key]['_mleft'] = $counter++; - foreach ($tree[$key]['_children'] as $child_key) { - _menu_renumber($tree, $child_key); - } - $tree[$key]['_mright'] = $counter++; - } -} -// Placeholders. +// Placeholders --> move these to menu.module function menu_primary_links() { } @@ -941,59 +754,76 @@ function menu_secondary_links() { * An array of links to the tabs. */ function menu_local_tasks($level = 0) { - static $tabs = array(), $parents = array(), $parents_done = array(); + static $tabs = array(); + if (empty($tabs)) { $router_item = menu_get_item(); if (!$router_item || !$router_item->access) { return array(); } - $map = arg(NULL); - do { - // Tabs are router items that have the same parent. If there is a new - // parent, let's add it the queue. - if (!empty($router_item->parent)) { - $parents[] = $router_item->parent; - // Do not add the same item twice. - $router_item->parent = ''; - } - $parent = array_shift($parents); - // Do not process the same parent twice. - if (isset($parents_done[$parent])) { - continue; + // Get all tabs + $result = db_query("SELECT * FROM {menu} WHERE tab_root = '%s' AND tab_parent != '' ORDER BY weight, title", $router_item->tab_root); + $map = arg(); + $children = array(); + $tab_parent = array(); + + while ($item = db_fetch_object($result)) { + $children[$item->tab_parent][$item->path] = $item; + $tab_parent[$item->path] = $item->tab_parent; + } + + // Find all tabs below the current path + $path = $router_item->path; + while (isset($children[$path])) { + $tabs_current = ''; + $next_path = ''; + foreach ($children[$path] as $item) { + _menu_translate($item, $map); + if ($item->access) { + $link = l($item->title, $item->link_path); // TODO options? + // The default task is always active. + if ($item->type == MENU_DEFAULT_LOCAL_TASK) { + $tabs_current .= theme('menu_local_task', $link, TRUE); + $next_path = $item->path; + } + else { + $tabs_current .= theme('menu_local_task', $link); + } + } } - // This loads all the tabs. - $result = db_query("SELECT * FROM {menu} WHERE parent = '%s' AND tab = 1 ORDER BY mleft", $parent); + $path = $next_path; + $tabs[$item->number_parts] = $tabs_current; + } + + // Find all tabs at the same level or above the current one + $parent = $router_item->tab_parent; + $path = $router_item->path; + $current = $router_item; + while (isset($children[$parent])) { $tabs_current = ''; - while ($item = db_fetch_object($result)) { - // This call changes the path from for example user/% to user/123 and - // also determines whether we are allowed to access it. - _menu_translate($item, $map, MENU_RENDER_LINK); + $next_path = ''; + $next_parent = ''; + foreach ($children[$parent] as $item) { + _menu_translate($item, $map); if ($item->access) { - $depth = $item->depth; - $link = l($item->title, $item->link_path, (array)$item); + $link = l($item->title, $item->link_path); // TODO options? // We check for the active tab. - if ($item->path == $router_item->path || (!$router_item->tab && $item->type == MENU_DEFAULT_LOCAL_TASK)) { + if ($item->path == $path) { $tabs_current .= theme('menu_local_task', $link, TRUE); - // Let's try to find the router item one level up. - $next_router_item = db_fetch_object(db_query("SELECT path, tab, parent FROM {menu} WHERE path = '%s'", $item->parent)); - // We will need to inspect one level down. - $parents[] = $item->path; + $next_path = $item->tab_parent; + if (isset($tab_parent[$next_path])) { + $next_parent = $tab_parent[$next_path]; + } } else { $tabs_current .= theme('menu_local_task', $link); } } } - // If there are tabs, let's add them - if ($tabs_current) { - $tabs[$depth] = $tabs_current; - } - $parents_done[$parent] = TRUE; - if (isset($next_router_item)) { - $router_item = $next_router_item; - } - unset($next_router_item); - } while ($parents); + $path = $next_path; + $parent = $next_parent; + $tabs[$item->number_parts] = $tabs_current; + } // Sort by depth ksort($tabs); // Remove the depth, we are interested only in their relative placement. @@ -1010,14 +840,62 @@ function menu_secondary_local_tasks() { return menu_local_tasks(1); } -function menu_set_active_item() { +function menu_set_active_menu_name($menu_name = NULL) { + static $active; + + if (isset($menu_name)) { + $names = menu_get_names(); + if (isset($names[$menu_name])) { + $active = $menu_name; + } + } + elseif (!isset($active)) { + $active = 'navigation'; + } + return $active; +} + +function menu_get_active_menu_name() { + return menu_set_active_menu_name(); +} + +function menu_set_active_trail($new_trail = NULL) { + static $trail; + + if (isset($new_trail)) { + $trail = $new_trail; + } + elseif (!isset($trail)) { + $item = menu_get_item(); + $trail = array(); + $tree = menu_tree_data(menu_get_active_menu_name()); + $curr = array_shift($tree); + while ($curr) { + if ($curr['below']) { + $trail[] = $curr['link']; + $tree = $curr['below']; + } + elseif ($curr['link']->path == $item->path){ + $trail[] = $curr['link']; + $curr = FALSE; + } + else { + $curr = array_shift($tree); + } + } + } + return $trail; +} + +function menu_get_active_trail() { + return menu_set_active_trail(); } function menu_set_location() { } function menu_get_active_breadcrumb() { - $breadcrumb = array(l(t('Home'), '')); + return $breadcrumb = array(l(t('Home'), '')); // TODO! $item = menu_get_item(); if ($item && $item->access) { foreach ($item->active_trail as $parent) { @@ -1029,29 +907,356 @@ function menu_get_active_breadcrumb() { function menu_get_active_title() { $item = menu_get_item(); + return $item->title; + // TODO + /* foreach (array_reverse($item->active_trail) as $item) { if (!($item->type & MENU_IS_LOCAL_TASK)) { return $item->title; } - } + }*/ } /** - * Get a menu item by its mid, access checked and link translated for + * Get a menu item by its mlid, access checked and link translated for * rendering. * - * @param $mid - * The mid of the menu item. + * @param $mlid + * The mlid of the menu item. * @return * A menu object, with $item->access filled and link translated for * rendering. */ -function menu_get_item_by_mid($mid) { - if ($item = db_fetch_object(db_query('SELECT * FROM {menu} WHERE mid = %d', $mid))) { - _menu_translate($item, arg(), MENU_RENDER_LINK); +function menu_get_item_by_mlid($mlid) { + if ($item = db_fetch_object(db_query("SELECT * FROM {menu_links} ml LEFT JOIN {menu} m ON m.path = ml.path WHERE mlid = %d", $mlid))) { + _menu_link_translate($item); if ($item->access) { return $item; } } return FALSE; } + +/** + * Populate the database representation of the menu. + */ +function menu_rebuild() { + $callbacks = module_invoke_all('menu'); + $names = menu_get_names(TRUE); + + foreach ($callbacks as $path => $item) { + if (!isset($item['menu name']) || !isset($names[$item['menu name']])) { + $callbacks[$path]['menu name'] = 'navigation'; // The default menu + } + } + + // Alter the menu as defined in modules, keys are like user/%user. + db_query('DELETE FROM {menu}'); + db_query('DELETE FROM {menu_links}'); + drupal_alter('menu', $callbacks); + $menu = _menu_router_build($callbacks); + + // Add normal and suggested items as links. + foreach ($menu as $path => $item) { + if ($item['_visible']) { + $item['link_text'] = $item['title']; + $menu_links[$item['menu name']][$path] = $item; + } + } + + foreach ($menu_links as $menu_name => $current_links) { + $mlid = 1; + // Find the access callback and other properties for each item. + foreach ($current_links as $link_path => $v) { + _menu_link_find_router_path($current_links[$link_path], $menu, $link_path); + } + $sort = array(); + // Find the parents and prepare to sort. + foreach ($current_links as $link_path => $v) { + _menu_link_find_parent($current_links[$link_path], $current_links, $link_path, $menu_name); + $item = $current_links[$link_path]; + // This sorting works correctly only with positive numbers, + // so we shift negative weights to be positive. + $sort[$link_path] = $item['_depth'] . sprintf('%05d', $item['weight'] + 50000) . $item['title']; + } + array_multisort($sort, $current_links); + // We are now sorted, so let's build the tree. + foreach ($current_links as $link_path => $item) { + if (!empty($item['_plid'])) { + $current_links[$item['parent']]['_children'][] = $link_path; + $current_links[$item['parent']]['has_children'] = TRUE; + } + } + // Number the tree. + menu_renumber($current_links); + foreach ($current_links as $link_path => $item) { + _menu_link_save($item, $link_path, $menu_name); + } + } +} + +function _menu_router_build($callbacks) { + // First pass: separate callbacks from pathes, making pathes ready for + // matching. Calculate fitness, and fill some default values. + $menu = array(); + foreach ($callbacks as $path => $item) { + $load_functions = array(); + $to_arg_functions = array(); + $fit = 0; + $move = FALSE; + + $parts = explode('/', $path, 6); + $number_parts = count($parts); + // We store the highest index of parts here to save some work in the fit + // calculation loop. + $slashes = $number_parts - 1; + // extract functions + foreach ($parts as $k => $part) { + $match = FALSE; + if (preg_match('/^%([a-z_]*)$/', $part, $matches)) { + if (empty($matches[1])) { + $match = TRUE; + $load_functions[$k] = NULL; + } + else { + if (function_exists($matches[1] .'_to_arg')) { + $to_arg_functions[$k] = $matches[1] .'_to_arg'; + $load_functions[$k] = NULL; + $match = TRUE; + } + if (function_exists($matches[1] .'_load')) { + $load_functions[$k] = $matches[1] .'_load'; + $match = TRUE; + } + } + } + if ($match) { + $parts[$k] = '%'; + } + else { + $fit |= 1 << ($slashes - $k); + } + } + if ($fit) { + $move = TRUE; + } + else { + // If there is no %, it fits maximally. + $fit = (1 << $number_parts) - 1; + } + $item['load_functions'] = empty($load_functions) ? '' : serialize($load_functions); + $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions); + $item += array( + 'title' => '', + 'weight' => 0, + 'type' => MENU_NORMAL_ITEM, + '_number_parts' => $number_parts, + '_parts' => $parts, + '_fit' => $fit, + ); + $item += array( + '_visible' => (bool)($item['type'] & MENU_VISIBLE_IN_TREE), + '_tab' => (bool)($item['type'] & MENU_IS_LOCAL_TASK), + ); + if ($move) { + $new_path = implode('/', $item['_parts']); + $menu[$new_path] = $item; + } + else { + $menu[$path] = $item; + } + } + + // Apply inheritance rules. + foreach ($menu as $path => $v) { + $item = &$menu[$path]; + if (!isset($item['access callback']) && isset($item['access arguments'])) { + $item['access callback'] = 'user_access'; // Default callback + } + if (!$item['_tab']) { + // Non-tab items + $item['tab_parent'] = ''; + $item['tab_root'] = $path; + } + for ($i = $item['_number_parts'] - 1; $i; $i--) { + $parent_path = implode('/', array_slice($item['_parts'], 0, $i)); + if (isset($menu[$parent_path])) { + + $parent = $menu[$parent_path]; + + if (!isset($item['tab_parent'])) { + // parent stores the parent of the path. + $item['tab_parent'] = $parent_path; + } + if (!isset($item['tab_root']) && !$parent['_tab']) { + $item['tab_root'] = $parent_path; + } + // If a callback is not found, we try to find the first parent that + // has a callback. + if (!isset($item['access callback']) && isset($parent['access callback'])) { + $item['access callback'] = $parent['access callback']; + if (!isset($item['access arguments']) && isset($parent['access arguments'])) { + $item['access arguments'] = $parent['access arguments']; + } + } + // Same for page callbacks. + if (!isset($item['page callback']) && isset($parent['page callback'])) { + $item['page callback'] = $parent['page callback']; + if (!isset($item['page arguments']) && isset($parent['page arguments'])) { + $item['page arguments'] = $parent['page arguments']; + } + } + } + } + if (!isset($item['access callback']) || empty($item['page callback'])) { + $item['access callback'] = 0; + } + if (is_bool($item['access callback'])) { + $item['access callback'] = intval($item['access callback']); + } + + $item += array( + 'access arguments' => array(), + 'access callback' => '', + 'page arguments' => array(), + 'page callback' => '', + 'block callback' => '', + 'title arguments' => array(), + 'title callback' => 't', + 'description' => '', + 'position' => '', + 'tab_parent' => '', + 'tab_root' => $path, + ); + db_query("INSERT INTO {menu} + (path, load_functions, to_arg_functions, access_callback, + access_arguments, page_callback, page_arguments, fit, + number_parts, tab_parent, tab_root, + title, title_callback, title_arguments, + type, block_callback, description, position, weight) + VALUES ('%s', '%s', '%s', '%s', + '%s', '%s', '%s', %d, + %d, '%s', '%s', + '%s', '%s', '%s', + %d, '%s', '%s', '%s', %d)", + $path, $item['load_functions'], $item['to_arg_functions'], $item['access callback'], + serialize($item['access arguments']), $item['page callback'], serialize($item['page arguments']), $item['_fit'], + $item['_number_parts'], $item['tab_parent'], $item['tab_root'], + $item['title'], $item['title callback'], serialize($item['title arguments']), + $item['type'], $item['block callback'], $item['description'], $item['position'], $item['weight']); + } + return $menu; +} + +function _menu_link_find_router_path(&$item, $menu, $path) { + static $mlid = 1; + $item['_mlid'] = $mlid++; + if (!isset($item['_external'])) { + $item['_external'] = menu_path_is_external($path); + } + if ($item['_external']) { + $item['path'] = ''; + } + else { + // Find the router path which will serve this path.s + $item['parts'] = explode('/', $path, 6); + $item['number_parts'] = count($item['parts']); + if (!isset($menu[$path]) || !isset($menu[$path]['access callback'])) { + list($ancestors) = menu_get_ancestors($item['parts']); + $counter = 20; + while ($ancestors && (!isset($menu[$path]) || !isset($menu[$path]['access callback']))) { + $path = array_pop($ancestors); + } + } + $item['path'] = $path; + } +} + +function _menu_link_find_parent(&$item, $current_links, $path, $menu_name) { + // Prepare for sorting and find parents. + $parents = array($item['_mlid']); + $depth = 1; + while ($path) { + if (isset($current_links[$path]['parent'])) { + $parent_path = $current_links[$path]['parent']; + } + else { + $parent_path = db_result(db_query("SELECT parent FROM {menu_links} WHERE menu_name = '%s' AND link_path = '%s'", $menu_name, $path)); + if ($parent_path === FALSE) { + $parent_path = substr($path, 0, strrpos($path, '/')); + } + } + if (isset($current_links[$parent_path])) { + $parent = $current_links[$parent_path]; + $parents[] = $parent['_mlid']; + $depth++; + if (!isset($item['_plid'])) { + $item['_plid'] = $parent['_mlid']; + $item['parent'] = $parent_path; + } + } + $path = $parent_path; + } + $parents[] = 0; + $parents = implode(',', array_reverse($parents)); + // Store variables and set defaults. + $item += array( + 'weight' => 0, + 'title' => '', + '_plid' => 0, + '_depth' => $depth, + '_parents' => $parents, + '_children' => array(), + 'has_children' => 0, + ); +} + +function _menu_link_save(&$item, $link_path, $menu_name) { + // set additional defaults + $item += array( + '_mleft' => 0, + '_mright' => 0, + 'title' => '', + 'weight' => 0, + 'options' => array(), + 'link_path' => $link_path, + 'parent' => '', + 'disabled' => FALSE, + ); + + db_query("INSERT INTO {menu_links} ( + menu_name, mlid, plid, link_path, parent, disabled, + external, path, parents, depth, has_children, weight, + mleft, mright, link_text, options) + VALUES ('%s', %d, %d, '%s', '%s', %d, + %d, '%s', '%s', %d, %d, %d, + %d, %d, '%s', '%s')", + $menu_name, $item['_mlid'], $item['_plid'], $link_path, $item['parent'], + $item['disabled'], $item['_external'], $item['path'], $item['_parents'], + $item['_depth'], $item['has_children'], $item['weight'], + $item['_mleft'], $item['_mright'], $item['link_text'], serialize($item['options'])); +} + +function menu_renumber(&$tree) { + foreach ($tree as $key => $element) { + if (!isset($tree[$key]['_mleft'])) { + _menu_renumber($tree, $key); + } + } +} + +function _menu_renumber(&$tree, $key) { + static $counter = 1; + if (!isset($tree[$key]['_mleft'])) { + $tree[$key]['_mleft'] = $counter++; + foreach ($tree[$key]['_children'] as $child_key) { + _menu_renumber($tree, $child_key); + } + $tree[$key]['_mright'] = $counter++; + } +} + +function menu_path_is_external($path) { + $colonpos = strpos($path, ':'); + return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && filter_xss_bad_protocol($path, FALSE) == check_plain($path); +} === modified file 'index.php' --- index.php 2007-04-06 13:27:20 +0000 +++ index.php 2007-05-06 03:48:08 +0000 @@ -12,6 +12,7 @@ require_once './includes/bootstrap.inc'; drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); +menu_rebuild(); $return = menu_execute_active_handler(); // Menu status constants are integers; page content is a string. === modified file 'modules/system/system.install' --- modules/system/system.install 2007-05-04 09:41:36 +0000 +++ modules/system/system.install 2007-05-06 03:48:08 +0000 @@ -336,8 +336,6 @@ function system_install() { ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); db_query("CREATE TABLE {menu} ( - mid int NOT NULL default 0, - pid int NOT NULL default 0, path varchar(255) NOT NULL default '', load_functions varchar(255) NOT NULL default '', to_arg_functions varchar(255) NOT NULL default '', @@ -347,32 +345,40 @@ function system_install() { page_arguments text, fit int NOT NULL default 0, number_parts int NOT NULL default 0, - mleft int NOT NULL default 0, - mright int NOT NULL default 0, - visible int NOT NULL default 0, - parents varchar(255) NOT NULL default '', - depth int NOT NULL default 0, - has_children int NOT NULL default 0, - tab int NOT NULL default 0, + tab_parent varchar(255) NOT NULL default '', + tab_root varchar(255) NOT NULL default '', title varchar(255) NOT NULL default '', title_callback varchar(255) NOT NULL default '', title_arguments varchar(255) NOT NULL default '', - parent varchar(255) NOT NULL default '', type int NOT NULL default 0, block_callback varchar(255) NOT NULL default '', description varchar(255) NOT NULL default '', position varchar(255) NOT NULL default '', - link_path varchar(255) NOT NULL default '', - attributes varchar(255) NOT NULL default '', - query varchar(255) NOT NULL default '', - fragment varchar(255) NOT NULL default '', - absolute INT NOT NULL default 0, - html INT NOT NULL default 0, + weight int NOT NULL default 0, PRIMARY KEY (path), KEY fit (fit), - KEY visible (visible), - KEY pid (pid), - KEY parent (parent) + KEY tab_parent (tab_parent) + ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); + + db_query("CREATE TABLE {menu_links} ( + menu_name varchar(64) NOT NULL default '', + mlid int NOT NULL default 0, + plid int NOT NULL default 0, + link_path varchar(255) NOT NULL default '', + parent varchar(255) NOT NULL default '', + path varchar(255) NOT NULL default '', + link_text varchar(255) NOT NULL default '', + disabled smallint NOT NULL default 0, + external smallint NOT NULL default 0, + mleft int NOT NULL default 0, + mright int NOT NULL default 0, + parents varchar(255) NOT NULL default '', + depth int NOT NULL default 0, + has_children int NOT NULL default 0, + weight int NOT NULL default 0, + options text, + PRIMARY KEY (menu_name, link_path), + KEY plid (plid) ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); db_query("CREATE TABLE {node} ( === modified file 'modules/system/system.module' --- modules/system/system.module 2007-05-04 09:41:36 +0000 +++ modules/system/system.module 2007-05-06 03:51:14 +0000 @@ -394,11 +394,9 @@ function system_main_admin_page($arg = N if (system_status(TRUE)) { drupal_set_message(t('One or more problems were detected with your Drupal installation. Check the status report for more information.', array('@status' => url('admin/logs/status'))), 'error'); } - - $map = arg(NULL); - $result = db_query("SELECT * FROM {menu} WHERE path LIKE 'admin/%%' AND depth = 2 AND visible = 1 AND path != 'admin/help' ORDER BY mleft"); + $result = db_query("SELECT * FROM {menu_links} ml INNER JOIN {menu} m ON ml.path = m.path WHERE link_path LIKE 'admin/%%' AND depth = 2 AND link_path != 'admin/help' ORDER BY mleft"); while ($item = db_fetch_object($result)) { - _menu_translate($item, $map, MENU_RENDER_LINK); + _menu_link_translate($item); if (!$item->access) { continue; } @@ -418,11 +416,10 @@ function system_main_admin_page($arg = N * Provide a single block on the administration overview page. */ function system_admin_menu_block($item) { - $map = arg(NULL); $content = array(); - $result = db_query('SELECT * FROM {menu} WHERE depth = %d AND %d < mleft AND mright < %d AND visible = 1 ORDER BY mleft', $item->depth + 1, $item->mleft, $item->mright); + $result = db_query("SELECT * FROM {menu_links} ml INNER JOIN {menu} m ON ml.path = m.path WHERE parent = '%s' AND menu_name = 'navigation' ORDER BY mleft, mright", $item->path); while ($item = db_fetch_object($result)) { - _menu_translate($item, $map, MENU_RENDER_LINK); + _menu_link_translate($item); if (!$item->access) { continue; } @@ -2345,14 +2342,14 @@ function theme_admin_block_content($cont $item['attributes'] = array(); } $item['attributes'] += array('title' => $item['description']); - $output .= '
  • '. l($item['title'], $item['path'], $item) .'
  • '; + $output .= '
  • '. l($item['title'], $item['link_path'], $item['options']) .'
  • '; } $output .= ''; } else { $output = '
    '; foreach ($content as $item) { - $output .= '
    '. l($item['title'], $item['path'], $item) .'
    '; + $output .= '
    '. l($item['title'], $item['link_path'], $item['options']) .'
    '; $output .= '
    '. $item['description'] .'
    '; } $output .= '
    ';