Commit 7c89dee5 authored by Dries's avatar Dries

- Patch #131009 by chx: menu system fixes/improvements.

parent e6759790
......@@ -307,6 +307,7 @@ function menu_get_item($path = NULL, $item = NULL) {
$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.
......@@ -320,8 +321,12 @@ function menu_get_item($path = NULL, $item = NULL) {
$items[$path] = FALSE;
return FALSE;
}
$item->access = $item->access && $parent->access;
$item->active_trail[] = $parent;
if ($parent->access) {
$item->active_trail[] = $parent;
}
else {
$item->access = FALSE;
}
}
if ($item->access) {
$item->map = $map;
......@@ -437,8 +442,15 @@ function _menu_translate(&$item, $map, $operation = MENU_HANDLE_REQUEST) {
*/
function menu_tree() {
if ($item = menu_get_item()) {
$args = explode(',', $item->parents);
$placeholders = implode(', ', array_fill(0, count($args), '%d'));
if ($item->access) {
$args = explode(',', $item->parents);
$placeholders = implode(', ', array_fill(0, count($args), '%d'));
}
// Show the root menu for access denied.
else {
$args = 0;
$placeholders = '%d';
}
list(, $menu) = _menu_tree(db_query('SELECT * FROM {menu} WHERE pid IN ('. $placeholders .') AND visible = 1 ORDER BY mleft', $args));
return $menu;
}
......@@ -548,7 +560,7 @@ function menu_get_active_help() {
$output = '';
$item = menu_get_item();
if (!$item->access) {
if (!$item || !$item->access) {
// Don't return help text for areas the user cannot access.
return;
}
......@@ -570,66 +582,80 @@ 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.
*/
function menu_rebuild() {
// TODO: split menu and menu links storage.
db_query('DELETE FROM {menu}');
$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) {
$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;
$fit = 0;
$load_functions = array();
$to_arg_functions = array();
// 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;
$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;
}
if (function_exists($matches[1] .'_load')) {
$load_functions[$k] = $matches[1] .'_load';
$match = TRUE;
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 ($match) {
$parts[$k] = '%';
if ($fit) {
$move = TRUE;
}
else {
$fit |= 1 << ($slashes - $k);
// 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);
// If there is no %, it fits maximally.
if (!$fit) {
$fit = (1 << $number_parts) - 1;
$move = FALSE;
}
else {
$move = TRUE;
}
$item += array(
'title' => '',
'weight' => 0,
......@@ -662,29 +688,31 @@ function menu_rebuild() {
// Second pass: prepare for sorting and find parents.
foreach ($menu as $path => $item) {
$item = &$menu[$path];
$number_parts = $item['_number_parts'];
if (isset($item['parent'])) {
if (isset($menu_path_map[$item['parent']])) {
$item['parent'] = $menu_path_map[$item['parent']];
}
$parent_parts = explode('/', $item['parent'], 6);
$slashes = count($parent_parts);
}
else {
$parent_parts = $item['_parts'];
$slashes = $number_parts - 1;
}
$depth = 1;
$parent_path = $path;
$parents = array($item['_mid']);
for ($i = $slashes; $i; $i--) {
$parent_path = implode('/', array_slice($parent_parts, 0, $i));
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;
$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;
}
}
}
}
......@@ -693,9 +721,8 @@ function menu_rebuild() {
// Store variables and set defaults.
$item += array(
'_pid' => 0,
'_depth' => ($item['_visible'] ? $depth : $number_parts),
'_depth' => ($item['_visible'] ? $depth : $item['_number_parts']),
'_parents' => $parents,
'_parent_parts' => $parent_parts,
'_slashes' => $slashes,
);
// This sorting works correctly only with positive numbers,
......@@ -708,7 +735,7 @@ function menu_rebuild() {
// We are now sorted, so let's build the tree.
$children = array();
foreach ($menu as $path => $item) {
if ($item['_pid']) {
if (!empty($item['_pid'])) {
$menu[$item['_visible_parent_path']]['_children'][] = $path;
}
}
......@@ -716,54 +743,60 @@ function menu_rebuild() {
// Apply inheritance rules.
foreach ($menu as $path => $item) {
$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 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.
if (!isset($item['access callback']) && isset($parent['access callback']) && !isset($parent['access inherited'])) {
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;
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.
if (!isset($item['access callback']) && isset($parent['access callback']) && !isset($parent['access inherited'])) {
if (isset($item['access arguments']) || !$parent['_visible']) {
$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'];
// 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 (empty($item['page callback'])) {
$item['access callback'] = 0;
}
}
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 (empty($item['page callback'])) {
$item['access callback'] = 0;
}
if ($item['_tab']) {
if (!isset($item['parent'])) {
$item['parent'] = implode('/', array_slice($item['_parts'], 0, $item['_number_parts'] - 1));
if (isset($item['parent'])) {
$item['_depth'] = $item['parent'] ? $menu[$item['parent']]['_depth'] + 1 : 1;
}
else {
$item['_depth'] = $item['parent'] ? $menu[$item['parent']]['_depth'] + 1 : 1;
$item['parent'] = implode('/', array_slice($item['_parts'], 0, $item['_number_parts'] - 1));
}
}
else {
......@@ -771,6 +804,7 @@ function menu_rebuild() {
// stored in parents, parent stores the tab parent.
$item['parent'] = $path;
}
$insert_item = $item;
unset($item);
$item = $insert_item + array(
......@@ -871,6 +905,9 @@ function menu_local_tasks($level = 0) {
static $tabs = array(), $parents = array(), $parents_done = 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
......@@ -943,8 +980,10 @@ function menu_set_location() {
function menu_get_active_breadcrumb() {
$breadcrumb = array(l(t('Home'), ''));
$item = menu_get_item();
foreach ($item->active_trail as $parent) {
$breadcrumb[] = l($parent->title, $parent->link_path, (array)$parent);
if ($item && $item->access) {
foreach ($item->active_trail as $parent) {
$breadcrumb[] = l($parent->title, $parent->link_path, (array)$parent);
}
}
return $breadcrumb;
}
......
......@@ -8,7 +8,7 @@ function menu_install() {
switch ($GLOBALS['db_type']) {
case 'mysql':
case 'mysqli':
db_query("CREATE TABLE {menu_edit} (
db_query("CREATE TABLE {menu_custom} (
path varchar(255) NOT NULL default '' ,
disabled int NOT NULL default 0,
title varchar(255) NOT NULL default '',
......@@ -21,7 +21,7 @@ function menu_install() {
) /*!40100 DEFAULT CHARACTER SET UTF8 */ ");
break;
case 'pgsql':
db_query("CREATE TABLE {menu_edit} (
db_query("CREATE TABLE {menu_custom} (
path varchar(255) NOT NULL default '' ,
disabled int NOT NULL default 0,
title varchar(255) NOT NULL default '',
......@@ -40,6 +40,6 @@ function menu_install() {
* Implementation of hook_uninstall().
*/
function menu_uninstall() {
db_query('DROP TABLE {menu_edit}');
db_query('DROP TABLE {menu_custom}');
menu_rebuild();
}
......@@ -87,12 +87,13 @@ function menu_menu() {
'page callback' => 'drupal_get_form',
'page arguments' => array('menu_item_delete_form'),
'type' => MENU_CALLBACK);
$result = db_query('SELECT * FROM {menu_edit} WHERE admin = 1');
$result = db_query('SELECT * FROM {menu_custom} WHERE admin = 1');
while ($item = db_fetch_array($result)) {
$item['access callback'] = 1;
$item['access inherited'] = TRUE;
$item['_custom_item'] = TRUE;
$item['_external'] = menu_path_is_external($item['path']);
$items[$item['path']] = $item;
}
return $items;
/*
$items[] = array('path' => 'admin/build/menu/menu/add',
......@@ -128,11 +129,29 @@ function menu_menu() {
* Implementation of hook_menu_alter.
*/
function menu_menu_alter(&$menu, $phase) {
if ($phase == MENU_ALTER_PREPROCESSED) {
$result = db_query('SELECT * FROM {menu_edit} me WHERE admin = 0');
while ($item = db_fetch_array($result)) {
$menu[$item['path']] = $item + $menu[$item['path']];
}
switch ($phase) {
case MENU_ALTER_MODULE_DEFINED:
foreach ($menu as $path => $item) {
if (isset($item['_custom_item']) && $item['_custom_item'] && !$item['_external']) {
list($ancestors, $placeholders) = menu_get_ancestors(explode('/', $path, 6));
// Remove the item itself, custom items need to inherit from an existing item.
array_shift($ancestors);
array_shift($placeholders);
$inherit_item = db_fetch_object(db_query_range('SELECT * FROM {menu} WHERE path IN ('. implode (',', $placeholders) .') ORDER BY fit DESC', $ancestors, 0, 1));
drupal_set_message(var_export($inherit_item, TRUE));
$menu[$path]['access callback'] = $inherit_item->access_callback;
$menu[$path]['access arguments'] = unserialize($inherit_item->access_arguments);
$menu[$path]['page callback'] = $inherit_item->page_callback;
$menu[$path]['page arguments'] = unserialize($inherit_item->page_arguments);
}
}
break;
case MENU_ALTER_PREPROCESSED:
$result = db_query('SELECT * FROM {menu_custom} me WHERE admin = 0');
while ($item = db_fetch_array($result)) {
$menu[$item['path']] = $item + $menu[$item['path']];
}
break;
}
}
......@@ -142,7 +161,7 @@ function menu_menu_alter(&$menu, $phase) {
*/
function menu_overview() {
$header = array(t('Menu item'), t('Expanded'), array('data' => t('Operations'), 'colspan' => '3'));
$result = db_query('SELECT m.*, me.disabled FROM {menu} m LEFT JOIN {menu_edit} me ON m.path = me.path WHERE visible = 1 OR (disabled = 1 AND admin = 0) ORDER BY mleft');
$result = db_query('SELECT m.*, me.disabled FROM {menu} m LEFT JOIN {menu_custom} me ON m.path = me.path WHERE visible = 1 OR (disabled = 1 AND admin = 0) ORDER BY mleft');
$map = arg();
$rows = array();
while ($item = db_fetch_object($result)) {
......@@ -201,17 +220,17 @@ function menu_overview() {
*/
function menu_flip_item($visible, $mid, $path = NULL) {
if (isset($mid)) {
$parent = menu_get_item_by_mid($mid);
$item = menu_get_item_by_mid($mid);
}
elseif (isset($path)) {
$parent = menu_get_item($path);
$item = menu_get_item($path);
}
if (isset($parent) && $parent->access) {
$result = db_query('SELECT * FROM {menu} WHERE %d <= mleft AND mright <= %d', $parent->mleft, $parent->mright);
if (isset($item) && $item->access) {
$result = db_query('SELECT child.*, parent.path AS parent_path FROM {menu} child INNER JOIN {menu} parent ON child.pid = parent.mid WHERE %d <= child.mleft AND child.mright <= %d', $item->mleft, $item->mright);
while ($item = db_fetch_object($result)) {
$update_result = db_query("UPDATE {menu_edit} SET disabled = %d WHERE path = '%s'", !$visible, $item->path);
$update_result = db_query("UPDATE {menu_custom} SET disabled = %d WHERE path = '%s'", !$visible, $item->path);
if (!db_affected_rows($update_result)) {
db_query("INSERT INTO {menu_edit} (parent, path, title, description, weight, type, admin, disabled) VALUES ('%s', '%s', '%s', '%s', %d, %d, %d, %d)", $item->parent, $item->path, $item->title, $item->description, 0, $item->type, 0, !$visible);
db_query("INSERT INTO {menu_custom} (parent, path, title, description, weight, type, admin, disabled) VALUES ('%s', '%s', '%s', '%s', %d, %d, %d, %d)", $item->parent_path, $item->path, $item->title, $item->description, 0, $item->type, 0, !$visible);
}
}
menu_rebuild();
......@@ -348,7 +367,7 @@ function menu_parent_options($mid, $pid = 0, $depth = 0) {
if ($mid && $mid == $pid) {
return $options;
}
$sql = 'SELECT m.*, me.disabled FROM {menu} m LEFT JOIN {menu_edit} me ON m.path = me.path WHERE (m.visible = 1 OR (me.disabled = 1 AND me.admin = 0))';
$sql = 'SELECT m.*, me.disabled FROM {menu} m LEFT JOIN {menu_custom} me ON m.path = me.path WHERE (m.visible = 1 OR (me.disabled = 1 AND me.admin = 0))';
if (!$mid) {
$params = array();
}
......@@ -398,11 +417,11 @@ function menu_edit_item_save($edit) {
$parent = $edit['pid'] ? db_result(db_query('SELECT path FROM {menu} WHERE mid = %d', $edit['pid'])) : '';
$t_args = array('%title' => $edit['title']);
if (!empty($edit['original_path']) && db_num_rows(db_query("SELECT * FROM {menu_edit} WHERE path='%s'", $edit['original_path']))) {
db_query("UPDATE {menu_edit} SET parent = '%s', title = '%s', description = '%s', weight = %d, type = %d, path = '%s' WHERE path = '%s'", $parent, $edit['title'], $edit['description'], $edit['weight'], $edit['type'], isset($edit['path']) ? $edit['path'] : $edit['original_path'], $edit['original_path']);
if (!empty($edit['original_path']) && db_num_rows(db_query("SELECT * FROM {menu_custom} WHERE path='%s'", $edit['original_path']))) {
db_query("UPDATE {menu_custom} SET parent = '%s', title = '%s', description = '%s', weight = %d, type = %d, path = '%s' WHERE path = '%s'", $parent, $edit['title'], $edit['description'], $edit['weight'], $edit['type'], isset($edit['path']) ? $edit['path'] : $edit['original_path'], $edit['original_path']);
}
else {
db_query("INSERT INTO {menu_edit} (parent, path, title, description, weight, type, admin) VALUES ('%s', '%s', '%s', '%s', %d, %d, %d)", $parent, isset($edit['path']) ? $edit['path'] : $edit['original_path'], $edit['title'], $edit['description'], $edit['weight'], $edit['type'], isset($edit['path']));
db_query("INSERT INTO {menu_custom} (parent, path, title, description, weight, type, admin) VALUES ('%s', '%s', '%s', '%s', %d, %d, %d)", $parent, isset($edit['path']) ? $edit['path'] : $edit['original_path'], $edit['title'], $edit['description'], $edit['weight'], $edit['type'], isset($edit['path']));
}
watchdog('menu', t('Saved menu item %title.', $t_args), WATCHDOG_NOTICE, l(t('view'), 'admin/build/menu'));
drupal_set_message(t('The menu item %title has been saved.', $t_args));
......@@ -522,7 +541,7 @@ function menu_reset_item_submit($form_id, $form_values) {
* The path to the menu item to be deleted.
*/
function menu_delete_item($path) {
db_query("DELETE FROM {menu_edit} WHERE path = '%s'", $path);
db_query("DELETE FROM {menu_custom} WHERE path = '%s'", $path);
menu_rebuild();
}
......
......@@ -343,6 +343,8 @@ function system_install() {
has_children int NOT NULL default 0,
tab int NOT NULL default 0,
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 '',
......@@ -812,6 +814,8 @@ function system_install() {
has_children int NOT NULL default 0,
tab int NOT NULL default 0,
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 '',
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment