diff --git a/includes/menu.inc b/includes/menu.inc index 1fe5a64..71245d3 100644 --- a/includes/menu.inc +++ b/includes/menu.inc @@ -317,7 +317,7 @@ define('MENU_PREFERRED_LINK', '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91'); * actually exists. This list of 'masks' is built in menu_rebuild(). * * @param $parts - * An array of path parts; for the above example, + * An array of path parts; for the above example, * array('node', '12345', 'edit'). * * @return @@ -2773,8 +2773,9 @@ function menu_rebuild() { try { list($menu, $masks) = menu_router_build(); - _menu_router_save($menu, $masks); - _menu_navigation_links_rebuild($menu); + $menu_needs_rebuild = _menu_router_save($menu, $masks); + _menu_navigation_links_rebuild($menu_needs_rebuild); + _menu_navigation_links_cleanup($menu); // Clear the menu, page and block caches. menu_cache_clear_all(); _menu_clear_page_cache(); @@ -2894,11 +2895,7 @@ function _menu_navigation_links_rebuild($menu) { array_multisort($sort, SORT_NUMERIC, $menu_links); foreach ($menu_links as $key => $item) { - $existing_item = db_select('menu_links') - ->fields('menu_links') - ->condition('link_path', $item['path']) - ->condition('module', 'system') - ->execute()->fetchAssoc(); + $existing_item = empty($item['_existing_item']) ? _menu_rebuild_get_existing_item($item['path']) : $item['_existing_item']; if ($existing_item) { $item['mlid'] = $existing_item['mlid']; // A change in hook_menu may move the link to a different menu @@ -2925,6 +2922,29 @@ function _menu_navigation_links_rebuild($menu) { } } } +} + +/** + * Gets an existing item for rebuild purposes. + * + * @param $path + * + * @return bool + */ +function _menu_rebuild_get_existing_item($path) { + return db_select('menu_links') + ->fields('menu_links') + ->condition('link_path', $path) + ->condition('module', 'system') + ->execute()->fetchAssoc(); +} + +/** + * Find new router paths where necessary and delete obsolete links. + * + * @param $menu + */ +function _menu_navigation_links_cleanup($menu) { $paths = array_keys($menu); // Updated and customized items whose router paths are gone need new ones. $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC)) @@ -3607,6 +3627,15 @@ function _menu_router_build($callbacks) { // matching. Calculate fitness, and fill some default values. $menu = array(); $masks = array(); + $link_fields = array_flip(drupal_schema_fields_sql('menu_links')); + unset($link_fields['weight'], $link_fields['module']); + $link_field_defaults = array( + 'node' => array('menu_name' => 'navigation'), + 'admin' => array('menu_name' => 'management'), + 'user' => array('menu_name' => 'user-menu'), + 'user/logout' => array('menu_name' => 'user-menu'), + 'user/%user' => array('menu_name' => 'navigation'), + ); foreach ($callbacks as $path => $item) { $load_functions = array(); $to_arg_functions = array(); @@ -3669,11 +3698,24 @@ function _menu_router_build($callbacks) { '_number_parts' => $number_parts, '_parts' => $parts, '_fit' => $fit, + '_keep' => FALSE, ); $item += array( '_visible' => (bool) ($item['type'] & MENU_VISIBLE_IN_BREADCRUMB), '_tab' => (bool) ($item['type'] & MENU_IS_LOCAL_TASK), ); + $link_fields_set = array_intersect_key($item, $link_fields); + if ($link_fields_set) { + if (isset($link_field_defaults[$path]) && $link_fields_set === $link_field_defaults[$path]) { + // Nothing to be done. + } + elseif ($item['_existing_item'] = _menu_rebuild_get_existing_item($path)) { + $item['_keep'] = (bool) array_diff_assoc($link_fields_set, $item['_existing_item']); + } + elseif ($item['_visible']) { + $item['_keep'] = TRUE; + } + } if ($move) { $new_path = implode('/', $item['_parts']); $menu[$new_path] = $item; @@ -3716,6 +3758,9 @@ function _menu_router_build($callbacks) { } // If the parent item defines a menu name, inherit it. if (!empty($parent['menu_name'])) { + if (!empty($parent['_keep'])) { + $item['_keep'] = TRUE; + } $item['menu_name'] = $parent['menu_name']; } } @@ -3831,6 +3876,12 @@ function _menu_router_build($callbacks) { */ function _menu_router_save($menu, $masks) { // Delete the existing router since we have some data to replace it. + $fields = drupal_schema_fields_sql('menu_router'); + db_truncate('menu_router_old')->execute(); + db_insert('menu_router_old') + ->fields($fields) + ->from(db_select('menu_router', 'r')->fields('r', $fields)) + ->execute(); db_truncate('menu_router')->execute(); // Prepare insert object. @@ -3862,8 +3913,11 @@ function _menu_router_save($menu, $masks) { )); $num_records = 0; - + $keep = array(); foreach ($menu as $path => $item) { + if (!empty($item['_keep'])) { + $keep[$path] = TRUE; + } // Fill in insert object values. $insert->values(array( 'path' => $item['path'], @@ -3902,8 +3956,17 @@ function _menu_router_save($menu, $masks) { $insert->execute(); // Store the masks. variable_set('menu_masks', $masks); - - return $menu; + $query = db_select('menu_router', 'new', array('fetch' => PDO::FETCH_ASSOC)); + // Weight is not necessary but we need path in the key of the resulting array + // and using fetchAllKeyed() is the cheapest way to do it. + $query->fields('new', array('path', 'weight')); + $query->leftJoin('menu_router_old', 'old', 'new.path = old.path'); + $or = db_or()->isNull('old.path'); + foreach ($fields as $field) { + $or->where("new.$field != old.$field"); + } + $query->condition($or); + return array_intersect_key($menu, $keep + $query->execute()->fetchAllKeyed()); } /** diff --git a/modules/system/system.install b/modules/system/system.install index 323b7b3..49eab8e 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -1140,6 +1140,8 @@ function system_schema() { ), 'primary key' => array('path'), ); + $schema['menu_router_old'] = $schema['menu_router']; + $schema['menu_router_old']['description'] = 'Menu router backup'; $schema['menu_links'] = array( 'description' => 'Contains the individual links within a menu.', @@ -3193,6 +3195,169 @@ function system_update_7080() { } /** + * Add the {menu_router_old} table. + */ +function system_update_7081() { + $table = array( + 'description' => 'Menu router backup', + 'fields' => array( + 'path' => array( + 'description' => 'Primary Key: the Drupal path this entry describes', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'load_functions' => array( + 'description' => 'A serialized array of function names (like node_load) to be called to load an object corresponding to a part of the current path.', + 'type' => 'blob', + 'not null' => TRUE, + ), + 'to_arg_functions' => array( + 'description' => 'A serialized array of function names (like user_uid_optional_to_arg) to be called to replace a part of the router path with another string.', + 'type' => 'blob', + 'not null' => TRUE, + ), + 'access_callback' => array( + 'description' => 'The callback which determines the access to this router path. Defaults to user_access.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'access_arguments' => array( + 'description' => 'A serialized array of arguments for the access callback.', + 'type' => 'blob', + 'not null' => FALSE, + ), + 'page_callback' => array( + 'description' => 'The name of the function that renders the page.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'page_arguments' => array( + 'description' => 'A serialized array of arguments for the page callback.', + 'type' => 'blob', + 'not null' => FALSE, + ), + 'delivery_callback' => array( + 'description' => 'The name of the function that sends the result of the page_callback function to the browser.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'fit' => array( + 'description' => 'A numeric representation of how specific the path is.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'number_parts' => array( + 'description' => 'Number of parts in this router path.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'small', + ), + 'context' => array( + 'description' => 'Only for local tasks (tabs) - the context of a local task to control its placement.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'tab_parent' => array( + 'description' => 'Only for local tasks (tabs) - the router path of the parent page (which may also be a local task).', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'tab_root' => array( + 'description' => 'Router path of the closest non-tab parent page. For pages that are not local tasks, this will be the same as the path.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'title' => array( + 'description' => 'The title for the current page, or the title for the tab if this is a local task.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'title_callback' => array( + 'description' => 'A function which will alter the title. Defaults to t()', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'title_arguments' => array( + 'description' => 'A serialized array of arguments for the title callback. If empty, the title will be used as the sole argument for the title callback.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'theme_callback' => array( + 'description' => 'A function which returns the name of the theme that will be used to render this page. If left empty, the default theme will be used.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'theme_arguments' => array( + 'description' => 'A serialized array of arguments for the theme callback.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'type' => array( + 'description' => 'Numeric representation of the type of the menu item, like MENU_LOCAL_TASK.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'description' => array( + 'description' => 'A description of this item.', + 'type' => 'text', + 'not null' => TRUE, + ), + 'position' => array( + 'description' => 'The position of the block (left or right) on the system administration page for this item.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'weight' => array( + 'description' => 'Weight of the element. Lighter weights are higher up, heavier weights go down.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'include_file' => array( + 'description' => 'The file to include for this element, usually the page callback function lives in this file.', + 'type' => 'text', + 'size' => 'medium', + ), + ), + 'indexes' => array( + 'fit' => array('fit'), + 'tab_parent' => array(array('tab_parent', 64), 'weight', 'title'), + 'tab_root_weight_title' => array(array('tab_root', 64), 'weight', 'title'), + ), + 'primary key' => array('path'), + ); + db_create_table('menu_router_old', $table); +} + +/** * @} End of "defgroup updates-7.x-extra". * The next series of updates should start at 8000. */