diff --git a/includes/menu.inc b/includes/menu.inc index d23edd0..3c79f87 100644 --- a/includes/menu.inc +++ b/includes/menu.inc @@ -1691,7 +1691,7 @@ function menu_cache_clear_all() { * menu_execute_active_handler(), because the maintenance page environment * is different and leaves stale data in the menu tables. */ -function menu_rebuild() { +function menu_rebuild($full = FALSE) { if (!lock_acquire('menu_rebuild')) { // Wait for another request that is already doing this work. // We choose to block here since otherwise the router item may not @@ -1700,12 +1700,17 @@ function menu_rebuild() { return FALSE; } - $menu = menu_router_build(TRUE); + $menu = menu_router_build(TRUE, $full); _menu_navigation_links_rebuild($menu); // Clear the menu, page and block caches. menu_cache_clear_all(); _menu_clear_page_cache(); + $checksums = _menu_router_build_checksum_cache(); + if (!empty($checksums)) { + cache_set('menu-rebuild:router-checksums', $checksums, 'cache_menu'); + } + if (defined('MAINTENANCE_MODE')) { variable_set('menu_rebuild_needed', TRUE); } @@ -1717,9 +1722,22 @@ function menu_rebuild() { } /** + * Helper function to store the cached menu checksums. + */ +function _menu_router_build_checksum_cache($checksums = NULL) { + static $checksum_cache = array(); + + if (isset($checksums)) { + $checksum_cache = $checksums; + } + + return $checksum_cache; +} + +/** * Collect, alter and store the menu definitions. */ -function menu_router_build($reset = FALSE) { +function menu_router_build($reset = FALSE, $full = FALSE) { static $menu; if (!isset($menu) || $reset) { @@ -1738,6 +1756,212 @@ function menu_router_build($reset = FALSE) { // Alter the menu as defined in modules, keys are like user/%user. drupal_alter('menu', $callbacks); $menu = _menu_router_build($callbacks); + + // We no longer need the callbacks. + unset($callbacks, $router_items, $module, $path); + + // Get the old menu from the cache if we can, otherwise assume an initial build + // and empty menu_router. + $menu_old_checksums = array( + 'router' => array(), + 'links' => array(), + ); + $flag_crud = TRUE; + if (!$full && $data = cache_get('menu-rebuild:router-checksums', 'cache_menu')) { + $menu_old_checksums = $data->data; + + // Unset $data to save on memory usage. + unset($data); + + // Empty the cache immediatly in case there is an error whilst processing changes + // so we don't get left in a indeterminant state. + cache_clear_all('menu-rebuild:router-checksums', 'cache_menu'); + } + else { + db_query('TRUNCATE TABLE {menu_router}'); + + // We can only do CRUD flagging when using the cached router entries. + $flag_crud = FALSE; + } + + // Set up a database map. + $db_map = array( + 'path' => 'path', + 'load_functions' => 'load_functions', + 'to_arg_functions' => 'to_arg_functions', + 'access callback' => 'access_callback', + 'access arguments' => 'access_arguments', + 'page callback' => 'page_callback', + 'page arguments' => 'page_arguments', + '_fit' => 'fit', + '_number_parts' => 'number_parts', + 'tab_parent' => 'tab_parent', + 'tab_root' => 'tab_root', + 'title' => 'title', + 'title callback' => 'title_callback', + 'title arguments' => 'title_arguments', + 'type' => 'type', + 'block callback' => 'block_callback', + 'description' => 'description', + 'position' => 'position', + 'weight' => 'weight', + 'include file' => 'file', + ); + + // Create the new menu checksums. + $menu_checksums = array(); + $menu_checksums_full = array(); + foreach (array_keys($menu) as $path) { + // Only include the items we care about. + $menu_checksums[$path] = md5(serialize(array( + 'path' => $menu[$path]['path'], + 'load_functions' => $menu[$path]['load_functions'], + 'to_arg_functions' => $menu[$path]['to_arg_functions'], + 'access callback' => $menu[$path]['access callback'], + 'access arguments' => $menu[$path]['access arguments'], + 'page callback' => $menu[$path]['page callback'], + 'page arguments' => $menu[$path]['page arguments'], + '_fit' => $menu[$path]['_fit'], + '_number_parts' => $menu[$path]['_number_parts'], + 'tab_parent' => $menu[$path]['tab_parent'], + 'tab_root' => $menu[$path]['tab_root'], + 'title' => $menu[$path]['title'], + 'title callback' => $menu[$path]['title callback'], + 'title arguments' => $menu[$path]['title arguments'], + 'type' => $menu[$path]['type'], + 'block callback' => $menu[$path]['block callback'], + 'description' => $menu[$path]['description'], + 'position' => $menu[$path]['position'], + 'weight' => $menu[$path]['weight'], + 'include file' => $menu[$path]['include file'], + ))); + + // Sort the item to preserve key order and keep the md5 as clean as possible. + ksort($menu[$path]); + $menu_checksums_full[$path] = md5(serialize($menu[$path])); + + // Handle CRUD flagging. + $menu[$path]['_crud'] = $flag_crud; + } + + // Cacluate the changes. + $changes_insert = array_diff(array_keys($menu_checksums), array_keys($menu_old_checksums['router'])); + $changes_delete = array_diff(array_keys($menu_old_checksums['router']), array_keys($menu_checksums)); + + // To calculate the update changes, we must first remove any inserted keys. + // We do this on a copy so that we can cache the original later. + $menu_checksums_copy = $menu_checksums; + foreach ($changes_insert as $path) { + unset($menu_checksums_copy[$path]); + } + + $changes_update = array_keys(array_diff_assoc($menu_checksums_copy, $menu_old_checksums['router'])); + + // Check for any paths that are not to be inserted, updated or deleted, that might have changed outside of + // the router. This will signify paths that need to be updated in the navigation links rebuild. + $menu_checksums_full_copy = $menu_checksums_full; + foreach (array_merge($changes_insert, $changes_delete, $changes_update) as $path) { + unset($menu_checksums_full_copy[$path]); + } + $changes_full_update = array_keys(array_diff_assoc($menu_checksums_full_copy, $menu_old_checksums['links'])); + foreach ($changes_full_update as $path) { + $menu[$path]['_updated_link'] = TRUE; + } + + // We no longer need $menu_old_checksumes or the copy. + unset($menu_old_checksums, $menu_checksums_copy, $menu_checksums_full_copy, $changes_full_update); + + // Perform the inserts. + if (!empty($changes_insert)) { + // The SQL is always the same, only the values change. + + // Map fields from the router build to the db equivelant. + $fields = array_values($db_map); + $value_fields = array_keys($db_map); + + $tokens = db_placeholders($fields, 'varchar'); + $fields = implode(', ', $fields); + $sql = "INSERT INTO {menu_router} ($fields) VALUES ($tokens)"; + + foreach ($changes_insert as $path) { + // Map values from the router build to the db equivalent. + $values = array(); + foreach ($value_fields as $mfield) { + $value = $menu[$path][$mfield]; + if ($mfield == 'access arguments' || $mfield == 'page arguments') { + $values[] = serialize($value); + } + elseif ($mfield == 'title arguments') { + $values[] = $value ? serialize($value) : ''; + } + elseif ($mfield == 'path') { + // Make sure that we insert the correct path. + $values[] = $path; + } + else { + $values[] = $value; + } + } + + db_query($sql, $values); + + // Flag this item as being inserted. + $menu[$path]['_inserted'] = TRUE; + } + } + + // We no longer need the insert changes. + unset($changes_insert, $fields, $value_fields, $tokens, $sql, $path, $mfield, $value, $values); + + // Perform the updates. + foreach ($changes_update as $path) { + $set = $args = array(); + foreach ($menu[$path] as $mfield => $value) { + if (isset($db_map[$mfield]) && $mfield != 'path') { + $set[] = $db_map[$mfield] ." = '%s'"; + if ($mfield == 'access arguments' || $mfield == 'page arguments') { + $args[] = serialize($value); + } + elseif ($mfield == 'title arguments') { + $args[] = $value ? serialize($value) : ''; + } + else { + $args[] = $value; + } + } + } + + if (!empty($set)) { + $set = implode(', ', $set); + $sql = "UPDATE {menu_router} SET $set WHERE path = '%s'"; + + $args[] = $path; + db_query($sql, $args); + + // Flag this item as being updated. + $menu[$path]['_updated'] = TRUE; + } + } + + // We no longer need the update changes. + unset($changes_update, $path, $set, $args, $db_map, $mfield, $value, $sql); + + // Perform the deletes. + $sql = "DELETE FROM {menu_router} WHERE path = '%s'"; + foreach ($changes_delete as $path) { + db_query($sql, $path); + + // Flag this item as being deleted. + $menu[$path]['_deleted'] = TRUE; + } + + // We no longer need the delete changes. + unset($changes_delete, $path, $sql); + + // Store the new rows in a cache for use as old rows on next rebuild. + // We delay the storage of this cache until after the main menu cache is cleared. + _menu_router_build_checksum_cache(array('router' => $menu_checksums, 'links' => $menu_checksums_full)); + _menu_router_cache($menu); } return $menu; @@ -1783,19 +2007,43 @@ function _menu_link_build($item) { */ function _menu_navigation_links_rebuild($menu) { // Add normal and suggested items as links. - $menu_links = array(); + $menu_links = $inserted = $updated = $deleted = array(); + $crud = FALSE; foreach ($menu as $path => $item) { + // Check if we can do CRUD actions. + if (!$crud && $item['_crud']) { + $crud = TRUE; + } + if ($item['_visible']) { $item = _menu_link_build($item); $menu_links[$path] = $item; $sort[$path] = $item['_number_parts']; + + if (!empty($item['_inserted'])) { + $inserted[] = $path; + } + elseif (!empty($item['_updated']) || !empty($item['_updated_link'])) { + $updated[$path] = $item['_number_parts']; + } + } + elseif (empty($item['_visible']) && !empty($item['_deleted'])) { + $deleted[] = $path; + unset($menu[$path]); } } - if ($menu_links) { + + if ($crud) { + // Only router items that have changed should be affected. + foreach ($inserted as $path) { + menu_link_save($menu_links[$path]); + } + // Make sure no child comes before its parent. - array_multisort($sort, SORT_NUMERIC, $menu_links); + asort($updated, SORT_NUMERIC); - foreach ($menu_links as $item) { + foreach ($updated as $path => $number_parts) { + $item = $menu_links[$path]; $existing_item = db_fetch_array(db_query("SELECT mlid, menu_name, plid, customized, has_children, updated FROM {menu_links} WHERE link_path = '%s' AND module = '%s'", $item['link_path'], 'system')); if ($existing_item) { $item['mlid'] = $existing_item['mlid']; @@ -1812,6 +2060,29 @@ function _menu_navigation_links_rebuild($menu) { } } } + else { + if ($menu_links) { + // Make sure no child comes before its parent. + array_multisort($sort, SORT_NUMERIC, $menu_links); + + foreach ($menu_links as $item) { + $existing_item = db_fetch_array(db_query("SELECT mlid, menu_name, plid, customized, has_children, updated FROM {menu_links} WHERE link_path = '%s' AND module = '%s'", $item['link_path'], 'system')); + if ($existing_item) { + $item['mlid'] = $existing_item['mlid']; + // A change in hook_menu may move the link to a different menu + if (empty($item['menu_name']) || ($item['menu_name'] == $existing_item['menu_name'])) { + $item['menu_name'] = $existing_item['menu_name']; + $item['plid'] = $existing_item['plid']; + } + $item['has_children'] = $existing_item['has_children']; + $item['updated'] = $existing_item['updated']; + } + if (!$existing_item || !$existing_item['customized']) { + menu_link_save($item); + } + } + } + } $placeholders = db_placeholders($menu, 'varchar'); $paths = array_keys($menu); // Updated and customized items whose router paths are gone need new ones. @@ -2359,8 +2630,7 @@ function _menu_router_build($callbacks) { watchdog('php', 'Menu router rebuild failed - some paths may not work correctly.', array(), WATCHDOG_ERROR); return array(); } - // Delete the existing router since we have some data to replace it. - db_query('DELETE FROM {menu_router}'); + // Apply inheritance rules. foreach ($menu as $path => $v) { $item = &$menu[$path]; @@ -2441,24 +2711,6 @@ function _menu_router_build($callbacks) { $file_path = $item['file path'] ? $item['file path'] : drupal_get_path('module', $item['module']); $item['include file'] = $file_path .'/'. $item['file']; } - - $title_arguments = $item['title arguments'] ? serialize($item['title arguments']) : ''; - db_query("INSERT INTO {menu_router} - (path, load_functions, to_arg_functions, access_callback, - access_arguments, page_callback, page_arguments, fit, - number_parts, tab_parent, tab_root, - title, title_callback, title_arguments, - type, block_callback, description, position, weight, file) - VALUES ('%s', '%s', '%s', '%s', - '%s', '%s', '%s', %d, - %d, '%s', '%s', - '%s', '%s', '%s', - %d, '%s', '%s', '%s', %d, '%s')", - $path, $item['load_functions'], $item['to_arg_functions'], $item['access callback'], - serialize($item['access arguments']), $item['page callback'], serialize($item['page arguments']), $item['_fit'], - $item['_number_parts'], $item['tab_parent'], $item['tab_root'], - $item['title'], $item['title callback'], $title_arguments, - $item['type'], $item['block callback'], $item['description'], $item['position'], $item['weight'], $item['include file']); } // Sort the masks so they are in order of descending fit, and store them. $masks = array_keys($masks);