Index: includes/menu.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/menu.inc,v retrieving revision 1.166 diff -u -p -r1.166 menu.inc --- includes/menu.inc 16 May 2007 13:45:16 -0000 1.166 +++ includes/menu.inc 19 May 2007 19:36:20 -0000 @@ -149,11 +149,6 @@ define('MENU_SITE_OFFLINE', 4); * @} End of "Menu status codes". */ - -/** - * @} End of "Menu operations." - */ - /** * @Name Menu tree parameters * @{ @@ -511,13 +506,13 @@ function _menu_link_translate(&$item) { $item->access = FALSE; return FALSE; } - if (!_menu_load_objects($item, $map)) { - // An error occured loading an object - $item->access = FALSE; - return FALSE; - } // TODO: menu_tree may set this ahead of time for links to nodes if (!isset($item->access)) { + if (!_menu_load_objects($item, $map)) { + // An error occured loading an object + $item->access = FALSE; + return FALSE; + } _menu_check_access($item, $map); } // If the link title matches that of a router item, localize it. @@ -578,6 +573,49 @@ function menu_tree_output($tree) { } /** + * Get the full data structure representing a named menu tree. Since this can be + * the full tree with no access checks done and including hidden items, the + * data returned may be used for generating an an admin interface or a select. + * + * @param $menu_name + * The named menu links to return + * @param $item + * A fully loaded menu link object, or NULL. If an object is supplied, the + * path to root will be set in the returned tree. + * @param $show_hidden + * @param $check_access + * @return + * An array of menu links, in the order they should be rendered. + */ +function menu_tree_all_data($menu_name = 'navigation', $item = NULL, $show_hidden = FALSE, $check_access = TRUE) { + static $tree = array(); + + $mlid = isset($item->mild) ? $item->mild : 0; + $key = $menu_name .':'. $mlid .':'. (int)$check_access .':'. (int)$show_hidden; + + if (!isset($tree[$key])) { + if ($mlid) { + $parents = array($item->p1, $item->p2, $item->p3, $item->p4, $item->p5); + } + else { + $parents = array(); + } + if (!$show_hidden) { + $where = ' AND ml.hidden = 0'; + } + else { + $where = ' AND ml.hidden != '.MENU_CALLBACK; + } + list(, $tree[$key]) = _menu_tree_data(db_query(" + SELECT *, ml.weight + 50000 AS weight FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path + WHERE ml.menu_name = '%s'". $where ." + ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC", $menu_name), $parents); + } + + return $tree[$key]; +} + +/** * Get the data structure representing a named menu tree, based on the current * page. The tree order is maintained by storing each parent in an invidual * field, see http://drupal.org/node/141866 for more. @@ -628,7 +666,7 @@ function menu_tree_data($menu_name = 'na // No need to order by p6 - there is a sort by weight later. list(, $tree[$menu_name]) = _menu_tree_data(db_query(" SELECT *, ml.weight + 50000 AS weight FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path - WHERE ml.menu_name = '%s' AND ml.plid IN (". $placeholders .") + WHERE ml.menu_name = '%s' AND ml.plid IN (". $placeholders .") AND ml.hidden = 0 ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC", $args), $parents); // TODO: cache_set() for the untranslated links @@ -898,6 +936,25 @@ function menu_secondary_local_tasks() { return menu_local_tasks(1); } +/** + * Returns the rendered local tasks. The default implementation renders + * them as tabs. + * + * @ingroup themeable + */ +function theme_menu_local_tasks() { + $output = ''; + + if ($primary = menu_primary_local_tasks()) { + $output .= "\n"; + } + if ($secondary = menu_secondary_local_tasks()) { + $output .= "\n"; + } + + return $output; +} + function menu_set_active_menu_name($menu_name = NULL) { static $active; @@ -1004,36 +1061,14 @@ function menu_get_active_title() { function menu_get_item_by_mlid($mlid) { if ($item = db_fetch_object(db_query("SELECT * FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE mlid = %d", $mlid))) { _menu_link_translate($item); - if ($item->access) { - return $item; - } + return $item; } return FALSE; } -/** - * Returns the rendered local tasks. The default implementation renders - * them as tabs. - * - * @ingroup themeable - */ -function theme_menu_local_tasks() { - $output = ''; - - if ($primary = menu_primary_local_tasks()) { - $output .= "\n"; - } - if ($secondary = menu_secondary_local_tasks()) { - $output .= "\n"; - } - - return $output; -} - function menu_cache_clear($menu_name = 'navigation') { - // TODO: starting stub. This will be called whenever an item is added to or - // moved from a named menu + cache_clear_all($menu_name .':', 'cache_menu', TRUE); } /** @@ -1041,8 +1076,7 @@ function menu_cache_clear($menu_name = ' * router items or menu links. */ function menu_cache_clear_all() { - cache_clear_all('*', 'menu_links', TRUE); - cache_clear_all('*', 'menu_router', TRUE); + cache_clear_all('*', 'cache_menu', TRUE); } /** @@ -1074,15 +1108,17 @@ function _menu_navigation_links_rebuild( if ($item['type'] == MENU_CALLBACK || $item['type'] == MENU_SUGGESTED_ITEM) { $item['hidden'] = $item['type']; } + // Note, we set this as 'system', so that we can be sure to distinguish all + // the menu links generated automatically from entries in {menu_router}. + $item['module'] = 'system'; $item += array( - 'menu name' => 'navigation', + 'menu_name' => 'navigation', 'link_title' => $item['title'], 'href' => $path, - 'module' => 'system', 'hidden' => 0, ); // We add nonexisting items. - if ($item['_visible'] && !db_result(db_query("SELECT COUNT(*) FROM {menu_links} WHERE menu_name = '%s' AND href = '%s'", $item['menu name'], $item['href']))) { + if ($item['_visible'] && !db_result(db_query("SELECT COUNT(*) FROM {menu_links} WHERE menu_name = '%s' AND href = '%s'", $item['menu_name'], $item['href']))) { $menu_links[$path] = $item; $sort[$path] = $item['_number_parts']; } @@ -1101,12 +1137,34 @@ function _menu_navigation_links_rebuild( } /** + * Delete a menu link. + * + * @param $mlid + * A valid menu link mlid + */ +function menu_link_delete($mlid) { + + $item = menu_get_item_by_mlid($mlid); + + // System-created items get automatically deleted, but only on menu rebuild. + if ($item && $item->module != 'system') { + + if ($item->has_children) { + // TODO - reparent children? + } + db_query('DELETE FROM {menu_links} WHERE mild = %d', $mlid); + menu_cache_clear($item->menu_name); + } +} + + +/** * Save a menu link. * * @param $item * An array representing a menu link item. The only mandatory keys are href * and link_title. Possible keys are - * menu name default is navigation + * menu_name default is navigation * weight default is 0 * expanded whether the item is expanded. * options An array of options, @see l for more. @@ -1130,7 +1188,7 @@ function menu_link_save(&$item, $_menu = $item['_external'] = menu_path_is_external($item['href']); // Load defaults. $item += array( - 'menu name' => 'navigation', + 'menu_name' => 'navigation', 'weight' => 0, 'link_title' => '', 'hidden' => 0, @@ -1138,26 +1196,24 @@ function menu_link_save(&$item, $_menu = 'expanded' => 0, 'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])), ); - $existing_item = array(); + $menu_name = $item['menu_name']; + $existing_item = FALSE; if (isset($item['mlid'])) { $existing_item = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $item['mlid'])); } else { - $existing_item = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE menu_name = '%s' AND href = '%s'", $item['menu name'], $item['href'])); + $existing_item = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE menu_name = '%s' AND href = '%s'", $menu_name, $item['href'])); } - if (empty($existing_item)) { + if (!$existing_item) { $item['mlid'] = db_next_id('{menu_links}_mlid'); } - $menu_name = $item['menu name']; - $new_path = !$existing_item || ($existing_item['href'] != $item['href']); - // Find the parent. if (isset($item['plid'])) { $parent = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $item['plid'])); } - else { // + else { $parent_path = $item['href']; do { $parent_path = substr($parent_path, 0, strrpos($parent_path, '/')); @@ -1180,21 +1236,24 @@ function menu_link_save(&$item, $_menu = } else { // Cannot add beyond the maximum depth. - if ($parent['depth'] >= (MENU_MAX_DEPTH)) { + if ($item['has_children'] && $existing_item) { + $limit = MENU_MAX_DEPTH - menu_children_relative_depth($existing_item) - 1; + } + else { + $limit = MENU_MAX_DEPTH - 1; + } + if ($parent['depth'] > $limit) { return FALSE; } $item['depth'] = $parent['depth'] + 1; - _menu_parents_copy($item, $parent); - $item['p'. $item['depth']] = $item['mlid']; + _menu_link_parents_set($item, $parent); } - - if ($item['plid'] != $existing_item['plid']) { - - // TODO: UPDATE the parents of the children of the current item - // TODO: check the has_children status of the previous parent + // Need to check both plid and menu_name, since plid can be 0 in any menu. + if ($existing_item && ($item['plid'] != $existing_item['plid'] || $menu_name != $existing_item['menu_name'])) { + _menu_link_move_children($item, $existing_item); } // Find the callback. - if (empty($item['router_path']) || $new_path) { + if (empty($item['router_path']) || !$existing_item || ($existing_item['href'] != $item['href'])) { if ($item['_external']) { $item['router_path'] = ''; } @@ -1213,13 +1272,13 @@ function menu_link_save(&$item, $_menu = } } } - if (!empty($existing_item)) { + if ($existing_item) { db_query("UPDATE {menu_links} SET menu_name = '%s', plid = %d, href = '%s', router_path = '%s', hidden = %d, external = %d, has_children = %d, expanded = %d, weight = %d, depth = %d, p1 = %d, p2 = %d, p3 = %d, p4 = %d, p5 = %d, p6 = %d, module = '%s', link_title = '%s', options = '%s' WHERE mlid = %d", - $item['menu name'], $item['plid'], $item['href'], + $item['menu_name'], $item['plid'], $item['href'], $item['router_path'], $item['hidden'], $item['_external'], $item['has_children'], $item['expanded'],$item['weight'], $item['depth'], $item['p1'], $item['p2'], $item['p3'], $item['p4'], $item['p5'], $item['p6'], @@ -1237,18 +1296,19 @@ function menu_link_save(&$item, $_menu = %d, %d, %d, %d, %d, %d, %d, %d, %d, '%s', '%s', '%s')", - $item['menu name'], $item['mlid'], $item['plid'], $item['href'], + $item['menu_name'], $item['mlid'], $item['plid'], $item['href'], $item['router_path'], $item['hidden'], $item['_external'], $item['has_children'], $item['expanded'],$item['weight'], $item['depth'], $item['p1'], $item['p2'], $item['p3'], $item['p4'], $item['p5'], $item['p6'], $item['module'], $item['link_title'], serialize($item['options'])); - } - - if ($item['plid'] && !$item['hidden']) { - db_query("UPDATE {menu_links} SET has_children = 1 WHERE mlid = %d", $item['plid']); + } + // Check the has_children status of the parent. + if ($item['plid']) { + $parent_has_children = (bool)db_result(db_query("SELECT COUNT(*) FROM {menu_links} WHERE plid = %d AND hidden = 0", $item['plid'])); + db_query("UPDATE {menu_links} SET has_children = %d WHERE mlid = %d", $parent_has_children, $item['plid']); } - // Keep track of which menus have expanded items + // Keep track of which menus have expanded items. $names = array(); $result = db_query("SELECT menu_name FROM {menu_links} WHERE expanded != 0 GROUP BY menu_name"); while ($n = db_fetch_array($result)) { @@ -1258,13 +1318,94 @@ function menu_link_save(&$item, $_menu = return TRUE; } -function _menu_parents_copy(&$dest, $source, $offset = 0){ - $i = 1 + $offset; - $depth = 0; - for ($j = 1; $i <= MENU_MAX_DEPTH; $i++) { - $dest['p'. $i] = $source['p'. $j]; - $j++; - } +/** + * Find the depth of an item's children relative to its depth. For example, if + * the item has a depth of 2, and the maximum of any child in the menu link tree + * is 5, the relative depth is 3. + * + * @param $item + * An array representing a menu link item. + * @return + * The relative depth, or zero. + * + */ +function menu_link_children_relative_depth($item) { + $i = 1; + $match = ''; + $args[] = $item['menu_name']; + $p = 'p1'; + while ($i <= MENU_MAX_DEPTH && $item[$p]) { + $match .= " AND $p = %d"; + $args[] = $item[$p]; + $p = 'p'. ++$i; + } + + $max_depth = db_result(db_query_range("SELECT depth FROM {menu_links} WHERE menu_name = '%s'". $match ." ORDER BY depth DESC", $args, 0, 1)); + + return ($max_depth > $item['depth']) ? $max_depth - $item['depth'] : 0; +} + +/** + * Update the menu name, parents (p1 - p6), and depth for the children of + * a menu link that's being moved in the tree and check the has_children status + * of the previous parent. + */ +function _menu_link_move_children($item, $existing_item) { + + $args[] = $item['menu_name']; + $set = ''; + $shift = $item['depth'] - $existing_item['depth']; + if ($shift < 0) { + $args[] = -$shift; + $set = ', depth = depth - %d'; + } + elseif ($shift > 0) { + $args[] = $shift; + $set = ', depth = depth + %d'; + } + $i = 1; + while ($i <= $item['depth']) { + $p = 'p'. $i++; + $set .= ", $p = %d"; + $args[] = $item[$p]; + } + $j = $existing_item['depth'] + 1; + while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) { + $set .= ', p'. $i++ .' = p'. $j++; + } + while ($i <= MENU_MAX_DEPTH) { + $set .= ', p'. $i++ .' = 0'; + } + + $args[] = $existing_item['menu_name']; + $i = 1; + while ($i <= MENU_MAX_DEPTH && $existing_item[$p]) { + $p = 'p'. $i++; + $match .= " AND $p = %d"; + $args[] = $existing_item[$p]; + } + + db_query("UPDATE {menu_links} SET menu_name = '%s'". $set ." WHERE menu_name = '%s'". $match, $args); + + if ($existing_item['plid']) { + $parent_has_children = (bool)db_result(db_query("SELECT COUNT(*) FROM {menu_links} WHERE plid = %d AND hidden = 0 AND mlid != %d",$existing_item['plid'], $existing_item['mlid'])); + db_query("UPDATE {menu_links} SET has_children = %d WHERE mlid = %d", $parent_has_children, $existing_item['plid']); + } +} + +function _menu_link_parents_set(&$item, $parent){ + $i = 1; + while ($i < $item['depth']) { + $p = 'p'. $i++; + $item[$p] = $parent[$p]; + } + $p = 'p'. $i++; + // The parent (p1 - p6) corresponding to the depth always equals the mlid. + $item[$p] = $item['mlid']; + while ($i <= MENU_MAX_DEPTH) { + $p = 'p'. $i++; + $item[$p] = 0; + } } function _menu_router_build($callbacks) { Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.112 diff -u -p -r1.112 system.install --- modules/system/system.install 18 May 2007 07:03:21 -0000 1.112 +++ modules/system/system.install 19 May 2007 19:36:21 -0000 @@ -265,6 +265,17 @@ function system_install() { INDEX expire (expire) ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); + db_query("CREATE TABLE {cache_menu} ( + cid varchar(255) BINARY NOT NULL default '', + data longblob, + expire int NOT NULL default '0', + created int NOT NULL default '0', + headers text, + serialized int(1) NOT NULL default '0', + PRIMARY KEY (cid), + INDEX expire (expire) + ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); + db_query("CREATE TABLE {comments} ( cid int NOT NULL auto_increment, pid int NOT NULL default '0', @@ -398,7 +409,9 @@ function system_install() { link_title varchar(255) NOT NULL default '', options text, PRIMARY KEY (mlid), - KEY parents (plid, p1, p2, p3, p4, p5), + KEY router_path (router_path), + KEY plid (plid), + KEY parents (p1, p2, p3, p4, p5), KEY menu_name_path (menu_name, href), KEY menu_expanded_children (expanded, has_children) ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); @@ -780,10 +793,19 @@ function system_install() { serialized smallint NOT NULL default '0', PRIMARY KEY (cid) )"); - db_query("CREATE INDEX {cache}_expire_idx ON {cache} (expire)"); + db_query("CREATE TABLE {cache_menu} ( + cid varchar(255) NOT NULL default '', + data bytea, + expire int NOT NULL default '0', + created int NOT NULL default '0', + headers text, + serialized smallint NOT NULL default '0', + PRIMARY KEY (cid) + )"); db_query("CREATE INDEX {cache}_expire_idx ON {cache} (expire)"); db_query("CREATE INDEX {cache_filter}_expire_idx ON {cache_filter} (expire)"); db_query("CREATE INDEX {cache_page}_expire_idx ON {cache_page} (expire)"); db_query("CREATE INDEX {cache_form}_expire_idx ON {cache_form} (expire)"); + db_query("CREATE INDEX {cache_menu}_expire_idx ON {cache_form} (expire)"); db_query("CREATE TABLE {comments} ( cid serial, @@ -919,7 +941,10 @@ function system_install() { options text, PRIMARY KEY (mlid) )"); - db_query("CREATE INDEX {menu_links}_parents_idx ON {menu_links} (plid, p1, p2, p3, p4, p5)"); + + db_query("CREATE INDEX {menu_links}_router_path_idx ON {menu_links} (router_path)"); + db_query("CREATE INDEX {menu_links}_plid_idx ON {menu_links} (plid)"); + db_query("CREATE INDEX {menu_links}_parents_idx ON {menu_links} (p1, p2, p3, p4, p5)"); db_query("CREATE INDEX {menu_links}_menu_name_idx ON {menu_links} (menu_name, href)"); db_query("CREATE INDEX {menu_links}_expanded_children_idx ON {menu_links} (expanded, has_children)"); @@ -3919,14 +3944,15 @@ function system_update_6012() { db_add_column($ret, 'cache', 'serialized', 'smallint', array('default' => "'0'", 'not null' => TRUE)); db_add_column($ret, 'cache_filter', 'serialized', 'smallint', array('default' => "'0'", 'not null' => TRUE)); db_add_column($ret, 'cache_page', 'serialized', 'smallint', array('default' => "'0'", 'not null' => TRUE)); + db_add_column($ret, 'cache_menu', 'serialized', 'smallint', array('default' => "'0'", 'not null' => TRUE)); break; case 'mysql': case 'mysqli': $ret[] = update_sql("ALTER TABLE {cache} ADD serialized int(1) NOT NULL default '0'"); $ret[] = update_sql("ALTER TABLE {cache_filter} ADD serialized int(1) NOT NULL default '0'"); $ret[] = update_sql("ALTER TABLE {cache_page} ADD serialized int(1) NOT NULL default '0'"); + $ret[] = update_sql("ALTER TABLE {cache_menu} ADD serialized int(1) NOT NULL default '0'"); break; - } return $ret;