=== modified file 'includes/menu.inc'
--- includes/menu.inc	2007-04-06 04:39:50 +0000
+++ includes/menu.inc	2007-04-13 01:33:23 +0000
@@ -307,6 +307,7 @@ function menu_get_item($path = NULL, $it
     $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, $it
           $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, $
  */
 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,20 +743,23 @@ 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'])) {
+    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 {
@@ -748,22 +778,23 @@ function menu_rebuild() {
           }
         }
       }
+      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 +802,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 +903,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 +978,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;
 }

=== modified file 'modules/menu/menu.install'
--- modules/menu/menu.install	2007-04-06 05:29:22 +0000
+++ modules/menu/menu.install	2007-04-09 06:28:05 +0000
@@ -1,5 +1,5 @@
 <?php
-// $Id: menu.install,v 1.1 2007/04/06 05:29:22 unconed Exp $
+// $Id$
 
 /**
  * Implementation of hook_install().
@@ -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();
 }

=== modified file 'modules/menu/menu.module'
--- modules/menu/menu.module	2007-04-06 13:27:20 +0000
+++ modules/menu/menu.module	2007-04-13 00:39:28 +0000
@@ -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 
   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
  *   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();
 }
 

=== modified file 'modules/system/system.install'
--- modules/system/system.install	2007-04-10 10:10:26 +0000
+++ modules/system/system.install	2007-04-13 01:41:55 +0000
@@ -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 '',

