Commit 03752e35 authored by Dries's avatar Dries

- Patch #34755 by chx et al: faster menu system. HEAD is temporary broken and...

- Patch #34755 by chx et al: faster menu system.  HEAD is temporary broken and there is no upgrade path yet.
parent d407de4c
......@@ -550,7 +550,7 @@ function drupal_page_cache_header($cache) {
* Define the critical hooks that force modules to always be loaded.
*/
function bootstrap_hooks() {
return array('init', 'exit');
return array('boot', 'exit');
}
/**
......@@ -856,7 +856,7 @@ function _drupal_cache_init($phase) {
}
elseif (variable_get('cache', CACHE_DISABLED) == CACHE_NORMAL) {
require_once './includes/module.inc';
bootstrap_invoke_all('init');
bootstrap_invoke_all('boot');
drupal_page_cache_header($cache);
bootstrap_invoke_all('exit');
exit();
......
......@@ -2209,3 +2209,22 @@ function element_child($key) {
function element_children($element) {
return array_filter(array_keys((array) $element), 'element_child');
}
/**
* Generate vancode.
*
* Consists of a leading character indicating length, followed by N digits
* with a numerical value in base 36. Vancodes can be sorted as strings
* without messing up numerical order.
*
* It goes:
* 00, 01, 02, ..., 0y, 0z,
* 110, 111, ... , 1zy, 1zz,
* 2100, 2101, ..., 2zzy, 2zzz,
* 31000, 31001, ...
*/
function int2vancode($i = 0) {
$num = base_convert((int)$i, 10, 36);
$length = strlen($num);
return chr($length + ord('0') - 1) . $num;
}
......@@ -119,7 +119,6 @@ function _locale_admin_manage_screen_submit($form_id, $form_values) {
drupal_set_message(t('Configuration saved.'));
// Changing the locale settings impacts the interface:
cache_clear_all('*', 'cache_menu', TRUE);
cache_clear_all('*', 'cache_page', TRUE);
return 'admin/settings/locale/language/overview';
......
......@@ -172,327 +172,220 @@
*/
/**
* Return the menu data structure.
*
* The returned structure contains much information that is useful only
* internally in the menu system. External modules are likely to need only
* the ['visible'] element of the returned array. All menu items that are
* accessible to the current user and not hidden will be present here, so
* modules and themes can use this structure to build their own representations
* of the menu.
*
* $menu['visible'] will contain an associative array, the keys of which
* are menu IDs. The values of this array are themselves associative arrays,
* with the following key-value pairs defined:
* - 'title' - The displayed title of the menu or menu item. It will already
* have been translated by the locale system.
* - 'description' - The description (link title attribute) of the menu item.
* It will already have been translated by the locale system.
* - 'path' - The Drupal path to the menu item. A link to a particular item
* can thus be constructed with
* l($item['title'], $item['path'], array('title' => $item['description'])).
* - 'children' - A linear list of the menu ID's of this item's children.
*
* Menu ID 0 is the "root" of the menu. The children of this item are the
* menus themselves (they will have no associated path). Menu ID 1 will
* always be one of these children; it is the default "Navigation" menu.
*/
function menu_get_menu() {
global $_menu;
global $user;
global $locale;
if (!isset($_menu['items'])) {
// _menu_build() may indirectly call this function, so prevent infinite loops.
$_menu['items'] = array();
$cid = "$user->uid:$locale";
if ($cached = cache_get($cid, 'cache_menu')) {
$_menu = unserialize($cached->data);
* Returns the ancestors (and relevant placeholders) for any given path.
*
* For example, the ancestors of node/12345/edit are:
*
* node/12345/edit
* node/12345/%
* node/%/edit
* node/%/%
* node/12345
* node/%
* node
*
* To generate these, we will use binary numbers. Each bit represents a
* part of the path. If the bit is 1, then it represents the original
* value while 0 means wildcard. If the path is node/12/edit/foo
* then the 1011 bitstring represents node/%/edit/foo where % means that
* any argument matches that part.
*
* @param $parts
* An array of path parts, for the above example
* array('node', '12345', 'edit').
* @return
* An array which contains the ancestors and placeholders. Placeholders
* simply contain as many %s as the ancestors.
*/
function menu_get_ancestors($parts) {
$n1 = count($parts);
$placeholders = array();
$ancestors = array();
$end = (1 << $n1) - 1;
$length = $n1 - 1;
for ($i = $end; $i > 0; $i--) {
$current = '';
$count = 0;
for ($j = $length; $j >= 0; $j--) {
if ($i & (1 << $j)) {
$count++;
$current .= $parts[$length - $j];
}
else {
$current .= '%';
}
if ($j) {
$current .= '/';
}
}
else {
_menu_build();
// Cache the menu structure for this user, to expire after one day.
cache_set($cid, 'cache_menu', serialize($_menu), time() + (60 * 60 * 24));
// If the number was like 10...0 then the next number will be 11...11,
// one bit less wide.
if ($count == 1) {
$length--;
}
// Make sure items that cannot be cached are added.
_menu_append_contextual_items();
// Reset the cached $menu in menu_get_item().
menu_get_item(NULL, NULL, TRUE);
$placeholders[] = "'%s'";
$ancestors[] = $current;
}
return $_menu;
return array($ancestors, $placeholders);
}
/**
* Return the local task tree.
* The menu system uses serialized arrays stored in the database for
* arguments. However, often these need to change according to the
* current path. This function unserializes such an array and does the
* necessary change.
*
* Unlike the rest of the menu structure, the local task tree cannot be cached
* nor determined too early in the page request, because the user's current
* location may be changed by a menu_set_location() call, and the tasks shown
* (just as the breadcrumb trail) need to reflect the changed location.
*/
function menu_get_local_tasks() {
global $_menu;
// Don't cache the local task tree, as it varies by location and tasks are
// allowed to be dynamically determined.
if (!isset($_menu['local tasks'])) {
// _menu_build_local_tasks() may indirectly call this function, so prevent
// infinite loops.
$_menu['local tasks'] = array();
$pid = menu_get_active_nontask_item();
if (!_menu_build_local_tasks($pid)) {
// If the build returned FALSE, the tasks need not be displayed.
$_menu['local tasks'][$pid]['children'] = array();
}
}
return $_menu['local tasks'];
}
/**
* Retrieves the menu item specified by $mid, or by $path if $mid is not given.
*
* @param $mid
* The menu ID of the menu item to retrieve.
* @param $path
* The internal path of the menu item to retrieve. Defaults to NULL. Only
* used if $mid is not set.
* @param $reset
* Optional flag that resets the static variable cache of the menu tree, if
* set to TRUE. Default is FALSE.
* 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').
*
* @param @data
* A serialized array.
* @param @map
* An array of potential replacements.
* @return
* The menu item found in the site menu, or an empty array if none could be
* found.
* The $data array unserialized and mapped.
*/
function menu_get_item($mid, $path = NULL, $reset = FALSE) {
static $menu;
if (!isset($menu) || $reset) {
$menu = menu_get_menu();
}
if (isset($mid)) {
return $menu['items'][$mid];
function menu_unserialize($data, $map) {
if ($data = unserialize($data)) {
foreach ($data as $k => $v) {
if (is_int($v)) {
$data[$k] = isset($map[$v]) ? $map[$v] : '';
}
}
return $data;
}
if (isset($path)) {
return $menu['items'][$menu['path index'][$path]];
else {
return array();
}
return array();
}
/**
* Retrieves the menu ID and title of all root menus.
* Replaces the statically cached item for a given path.
*
* @return
* Array containing all menus (but not menu items), in the form mid => title.
* @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.
*/
function menu_get_root_menus() {
$menu = menu_get_menu();
$root_menus = array();
foreach ($menu['items'][0]['children'] as $mid) {
$root_menus[$mid] = $menu['items'][$mid]['title'];
}
return $root_menus;
function menu_set_item($path, $item) {
menu_get_item($path, TRUE, $item);
}
/**
* Change the current menu location of the user.
*
* Frequently, modules may want to make a page or node act as if it were
* in the menu tree somewhere, even though it was not registered in a
* hook_menu() implementation. If the administrator has rearranged the menu,
* the newly set location should respect this in the breadcrumb trail and
* expanded/collapsed status of menu items in the tree. This function
* allows this behavior.
*
* @param $location
* An array specifying a complete or partial breadcrumb trail for the
* new location, in the same format as the return value of hook_menu().
* The last element of this array should be the new location itself.
*
* This function will set the new breadcrumb trail to the passed-in value,
* but if any elements of this trail are visible in the site tree, the
* trail will be "spliced in" to the existing site navigation at that point.
*/
function menu_set_location($location) {
global $_menu;
$temp_id = min(array_keys($_menu['items'])) - 1;
$prev_id = 0;
// Don't allow this function to change the actual current path, just the
// position in the menu tree.
$location[count($location) - 1]['path'] = $_GET['q'];
foreach (array_reverse($location) as $item) {
if (isset($_menu['path index'][$item['path']])) {
$mid = $_menu['path index'][$item['path']];
if (isset($_menu['visible'][$mid])) {
// Splice in the breadcrumb at this location.
if ($prev_id) {
$_menu['items'][$prev_id]['pid'] = $mid;
}
$prev_id = 0;
break;
}
else {
// A hidden item; show it, but only temporarily.
$_menu['items'][$mid]['type'] |= MENU_VISIBLE_IN_BREADCRUMB;
if ($prev_id) {
$_menu['items'][$prev_id]['pid'] = $mid;
}
$prev_id = $mid;
function menu_get_item($path = NULL, $execute = TRUE, $item = NULL) {
static $items;
if (!isset($path)) {
$path = $_GET['q'];
}
if (isset($item)) {
$items[$path] = $item;
}
if (!isset($items[$path])) {
$map = arg(NULL, $path);
$parts = array_slice($map, 0, 6);
list($ancestors, $placeholders) = menu_get_ancestors($parts);
if ($item = db_fetch_object(db_query_range('SELECT * FROM {menu} WHERE path IN ('. implode (',', $placeholders) .') ORDER BY fit DESC', $ancestors, 0, 1))) {
$item->access = _menu_access($item, $map);
if ($map === FALSE) {
$items[$path] = FALSE;
return FALSE;
}
}
else {
$item['type'] |= MENU_VISIBLE_IN_BREADCRUMB;
if ($prev_id) {
$_menu['items'][$prev_id]['pid'] = $temp_id;
if ($execute) {
$item->page_arguments = array_merge(menu_unserialize($item->page_arguments, $map), array_slice($parts, $item->number_parts));
}
$_menu['items'][$temp_id] = $item;
$_menu['path index'][$item['path']] = $temp_id;
$prev_id = $temp_id;
$temp_id--;
}
$items[$path] = $item;
}
if ($prev_id) {
// Didn't find a home, so attach this to the main navigation menu.
$_menu['items'][$prev_id]['pid'] = 1;
}
$final_item = array_pop($location);
menu_set_active_item($final_item['path']);
return $items[$path];
}
/**
* Execute the handler associated with the active menu item.
*
* This is called early in the page request. The active menu item is at
* this point determined exclusively by the URL. The handler that is called
* here may, as a side effect, change the active menu item so that later
* menu functions (that display the menus and breadcrumbs, for example)
* act as if the user were in a different location on the site.
*/
function menu_execute_active_handler() {
if (_menu_site_is_offline()) {
return MENU_SITE_OFFLINE;
}
$menu = menu_get_menu();
// Determine the menu item containing the callback.
$path = $_GET['q'];
while ($path && !isset($menu['callbacks'][$path])) {
$path = substr($path, 0, strrpos($path, '/'));
}
if (!isset($menu['callbacks'][$path])) {
return MENU_NOT_FOUND;
if ($item = menu_get_item()) {
return $item->access ? call_user_func_array($item->page_callback, $item->page_arguments) : MENU_ACCESS_DENIED;
}
return MENU_NOT_FOUND;
}
if (!function_exists($menu['callbacks'][$path]['callback'])) {
return MENU_NOT_FOUND;
function _menu_access($item, &$map) {
if ($item->map_callback) {
$map = call_user_func_array($item->map_callback, array_merge(array($map), unserialize($item->map_arguments)));
if ($map === FALSE) {
return FALSE;
}
}
if (!_menu_item_is_accessible(menu_get_active_item())) {
return MENU_ACCESS_DENIED;
$callback = $item->access_callback;
if (is_numeric($callback)) {
return $callback;
}
// We found one, and are allowed to execute it.
$arguments = isset($menu['callbacks'][$path]['callback arguments']) ? $menu['callbacks'][$path]['callback arguments'] : array();
$arg = substr($_GET['q'], strlen($path) + 1);
if (strlen($arg)) {
$arguments = array_merge($arguments, explode('/', $arg));
$arguments = menu_unserialize($item->access_arguments, $map);
// As call_user_func_array is quite slow and user_access is a very common
// callback, it is worth making a special case for it.
if ($callback == 'user_access') {
return (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
}
return call_user_func_array($menu['callbacks'][$path]['callback'], $arguments);
return call_user_func_array($callback, $arguments);
}
/**
* Returns the ID of the active menu item.
* Returns a rendered menu tree.
*/
function menu_get_active_item() {
return menu_set_active_item();
function menu_tree() {
$item = menu_get_item();
list(, $menu) = _menu_tree(db_query('SELECT * FROM {menu} WHERE pid IN ('. $item->parents .') AND visible = 1 ORDER BY vancode'));
return $menu;
}
/**
* Sets the path of the active menu item.
*/
function menu_set_active_item($path = NULL) {
static $stored_mid;
if (!isset($stored_mid) || isset($path)) {
if (!isset($path)) {
$path = $_GET['q'];
function _menu_tree($result = NULL, $depth = 0, $link = array('link' => '', 'has_children' => FALSE)) {
static $original_map;
$remnant = array('link' => '', 'has_children' => FALSE);
$tree = '';
while ($item = db_fetch_object($result)) {
$map = arg(NULL, $item->path);
if (!_menu_access($item, $map)) {
continue;
}
else {
$_GET['q'] = $path;
$menu_link = array('link' => $item->menu_link, 'has_children' => $item->has_children);
if ($item->depth > $depth) {
list($remnant, $menu) = _menu_tree($result, $item->depth, $menu_link);
$tree .= theme('menu_tree', $link, $menu);
$link = $remnant;
$remnant = '';
}
$menu = menu_get_menu();
while ($path && !isset($menu['path index'][$path])) {
$path = substr($path, 0, strrpos($path, '/'));
elseif ($item->depth == $depth) {
$tree .= theme('menu_link', $link);
$link = $menu_link;
}
$stored_mid = isset($menu['path index'][$path]) ? $menu['path index'][$path] : 0;
// Search for default local tasks to activate instead of this item.
$continue = TRUE;
while ($continue) {
$continue = FALSE;
if (isset($menu['items'][$stored_mid]['children'])) {
foreach ($menu['items'][$stored_mid]['children'] as $cid) {
if ($menu['items'][$cid]['type'] & MENU_LINKS_TO_PARENT) {
$stored_mid = $cid;
$continue = TRUE;
}
}
}
else {
$remnant = $menu_link;
break;
}
// Reset the cached $menu in menu_get_item().
menu_get_item(NULL, NULL, TRUE);
}
return $stored_mid;
if ($link['link']) {
$tree .= theme('menu_link', $link);
}
return array($remnant, $tree);
}
/**
* Returns the ID of the current menu item or, if the current item is a
* local task, the menu item to which this task is attached.
* Generate the HTML for a menu tree.
*/
function menu_get_active_nontask_item() {
$mid = menu_get_active_item();
// Find the first non-task item:
while ($mid) {
$item = menu_get_item($mid);
if (!($item['type'] & MENU_IS_LOCAL_TASK)) {
return $mid;
}
$mid = $item['pid'];
}
function theme_menu_tree($link, $tree) {
$tree = '<ul class="menu">'. $tree .'</ul>';
return $link['link'] ? theme('menu_link', $link, $tree) : $tree;
}
/**
* Returns the title of the active menu item.
* Generate the HTML for a menu link.
*/
function menu_get_active_title() {
if ($mid = menu_get_active_nontask_item()) {
$item = menu_get_item($mid);
return $item['title'];
}
function theme_menu_link($link, $menu = '') {
return '<li class="'. ($menu ? 'expanded' : ($link['has_children'] ? 'collapsed' : 'leaf')) .'">'. $link['link'] . $menu .'</li>' . "\n";
}
/**
......@@ -501,8 +394,9 @@ function menu_get_active_title() {
function menu_get_active_help() {
$path = $_GET['q'];
$output = '';
$item = menu_get_item();
if (!_menu_item_is_accessible(menu_get_active_item())) {
if (!$item->access) {
// Don't return help text for areas the user cannot access.
return;
}
......@@ -510,7 +404,7 @@ function menu_get_active_help() {
foreach (module_list() as $name) {
if (module_hook($name, 'help')) {
if ($temp = module_invoke($name, 'help', $path)) {
$output .= $temp . "\n";
$output .= $temp ."\n";
}
if (module_hook('help', 'page')) {
if (arg(0) == "admin") {
......@@ -524,869 +418,167 @@ function menu_get_active_help() {
return $output;
}
/**
* Returns an array of rendered menu items in the active breadcrumb trail.
*/
function menu_get_active_breadcrumb() {
// No breadcrumb for the front page.
if (drupal_is_front_page()) {
return array();
}
// We do *not* want to use "variable_get('site_frontpage', 'node)" here
// as that will create the link '/node'. This is unsightly and creates
// a second URL for the homepage ('/' *and* '/node').
$links[] = l(t('Home'), '');
$trail = _menu_get_active_trail();
foreach ($trail as $mid) {
$item = menu_get_item($mid);
if ($item['type'] & MENU_VISIBLE_IN_BREADCRUMB) {
$links[] = menu_item_link($mid);
}
}
// The last item in the trail is the page title; don't display it here.
array_pop($links);
return $links;
}
/**
* Returns TRUE when the menu item is in the active trail.
*/
function menu_in_active_trail($mid) {
$trail = _menu_get_active_trail();
return in_array($mid, $trail);
}
/**
* Returns TRUE when the menu item is in the active trail within a
* specific subsection of the menu tree.
*
* @param $mid
* The menu item being considered.
* @param $pid
* The root of the subsection of the menu tree in which to look.
*/
function menu_in_active_trail_in_submenu($mid, $pid) {
$trail = _menu_get_active_trail_in_submenu($pid);
if (!$trail) {
return FALSE;
}
return in_array($mid, $trail);
}
/**
* Populate the database representation of the menu.
*
* This need only be called at the start of pages that modify the menu.
*/
function menu_rebuild() {
// Clear the page cache, so that changed menus are reflected for anonymous users.
cache_clear_all('*', 'cache_page', TRUE);
// Also clear the menu cache.
cache_clear_all('*', 'cache_menu', TRUE);
_menu_build();
if (module_exists('menu')) {
$menu = menu_get_menu();
// Fill a queue of new menu items which are modifiable.
$new_items = array();
foreach ($menu['items'] as $mid => $item) {
if ($mid < 0 && ($item['type'] & MENU_MODIFIABLE_BY_ADMIN)) {