Index: menu.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/menu.inc,v retrieving revision 1.255.2.27 diff -u -r1.255.2.27 menu.inc --- menu.inc 10 Nov 2008 17:30:39 -0000 1.255.2.27 +++ menu.inc 13 Jul 2009 19:13:49 -0000 @@ -1652,10 +1652,13 @@ */ function menu_rebuild() { variable_del('menu_rebuild_needed'); + $changes = new stdClass; + $menu = menu_router_build(TRUE, $changes); + require_once 'includes/menu.rebuild_links.inc'; + // Right now the info in $changes is not actually used... + _menu_links_build_rebuild($menu, $changes); + // Clear the menu, page and block caches. menu_cache_clear_all(); - $menu = menu_router_build(TRUE); - _menu_navigation_links_rebuild($menu); - // Clear the page and block caches. _menu_clear_page_cache(); if (defined('MAINTENANCE_MODE')) { variable_set('menu_rebuild_needed', TRUE); @@ -1666,29 +1669,12 @@ * Collect, alter and store the menu definitions. */ function menu_router_build($reset = FALSE) { + require_once 'includes/menu.rebuild_router.inc'; static $menu; if (!isset($menu) || $reset) { - if (!$reset && ($cache = cache_get('router:', 'cache_menu')) && isset($cache->data)) { - $menu = $cache->data; - } - else { - // We need to manually call each module so that we can know which module - // a given item came from. - $callbacks = array(); - foreach (module_implements('menu') as $module) { - $router_items = call_user_func($module .'_menu'); - if (isset($router_items) && is_array($router_items)) { - foreach (array_keys($router_items) as $path) { - $router_items[$path]['module'] = $module; - } - $callbacks = array_merge($callbacks, $router_items); - } - } - // Alter the menu as defined in modules, keys are like user/%user. - drupal_alter('menu', $callbacks); - $menu = _menu_router_build($callbacks); - } + $menu = _menu_router_build($changes); + _menu_router_cache($menu); } return $menu; } @@ -2193,205 +2179,47 @@ } } + /** * Helper function to build the router table based on the data from hook_menu. - */ -function _menu_router_build($callbacks) { - // First pass: separate callbacks from paths, making paths ready for - // matching. Calculate fitness, and fill some default values. - $menu = array(); - foreach ($callbacks as $path => $item) { - $load_functions = array(); - $to_arg_functions = array(); - $fit = 0; - $move = FALSE; - - $parts = explode('/', $path, MENU_MAX_PARTS); - $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 load and to_arg functions. - foreach ($parts as $k => $part) { - $match = FALSE; - // Look for wildcards in the form allowed to be used in PHP functions, - // because we are using these to construct the load function names. - // See http://php.net/manual/en/language.functions.php for reference. - if (preg_match('/^%(|[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$/', $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; - $match = TRUE; - } - if (function_exists($matches[1] .'_load')) { - $function = $matches[1] .'_load'; - // Create an array of arguments that will be passed to the _load - // function when this menu path is checked, if 'load arguments' - // exists. - $load_functions[$k] = isset($item['load arguments']) ? array($function => $item['load arguments']) : $function; - $match = TRUE; - } - } - } - if ($match) { - $parts[$k] = '%'; - } - else { - $fit |= 1 << ($slashes - $k); - } - } - if ($fit) { - $move = TRUE; - } - else { - // If there is no %, it fits maximally. - $fit = (1 << $number_parts) - 1; - } - $masks[$fit] = 1; - $item['load_functions'] = empty($load_functions) ? '' : serialize($load_functions); - $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions); - $item += array( - 'title' => '', - 'weight' => 0, - 'type' => MENU_NORMAL_ITEM, - '_number_parts' => $number_parts, - '_parts' => $parts, - '_fit' => $fit, - ); - $item += array( - '_visible' => (bool)($item['type'] & MENU_VISIBLE_IN_BREADCRUMB), - '_tab' => (bool)($item['type'] & MENU_IS_LOCAL_TASK), - ); - if ($move) { - $new_path = implode('/', $item['_parts']); - $menu[$new_path] = $item; - $sort[$new_path] = $number_parts; - } - else { - $menu[$path] = $item; - $sort[$path] = $number_parts; - } - } - array_multisort($sort, SORT_NUMERIC, $menu); - + * To be called by menu_router_build() + * + * @param $changes object to collect $insert, $update, $delete arrays + */ +function _menu_router_build($changes = NULL) { + + $callbacks = _menu_router_build_calc_callbacks(); + list($menu, $masks) = _menu_router_build_calc_menu($callbacks); + if (!$menu) { // We must have a serious error - there is no data to save. 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]; - if (!$item['_tab']) { - // Non-tab items. - $item['tab_parent'] = ''; - $item['tab_root'] = $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 (!isset($item['tab_parent'])) { - // Parent stores the parent of the path. - $item['tab_parent'] = $parent_path; - } - if (!isset($item['tab_root']) && !$parent['_tab']) { - $item['tab_root'] = $parent_path; - } - // If an access callback is not found for a default local task we use - // the callback from the parent, since we expect them to be identical. - // In all other cases, the access parameters must be specified. - if (($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access callback']) && isset($parent['access callback'])) { - $item['access callback'] = $parent['access callback']; - if (!isset($item['access arguments']) && isset($parent['access arguments'])) { - $item['access arguments'] = $parent['access arguments']; - } - } - // Same 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['file']) && isset($parent['file'])) { - $item['file'] = $parent['file']; - } - if (!isset($item['file path']) && isset($parent['file path'])) { - $item['file path'] = $parent['file path']; - } - } - } - } - if (!isset($item['access callback']) && isset($item['access arguments'])) { - // Default callback. - $item['access callback'] = 'user_access'; - } - if (!isset($item['access callback']) || empty($item['page callback'])) { - $item['access callback'] = 0; - } - if (is_bool($item['access callback'])) { - $item['access callback'] = intval($item['access callback']); - } - - $item += array( - 'access arguments' => array(), - 'access callback' => '', - 'page arguments' => array(), - 'page callback' => '', - 'block callback' => '', - 'title arguments' => array(), - 'title callback' => 't', - 'description' => '', - 'position' => '', - 'tab_parent' => '', - 'tab_root' => $path, - 'path' => $path, - 'file' => '', - 'file path' => '', - 'include file' => '', - ); - - // Calculate out the file to be included for each callback, if any. - if ($item['file']) { - $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); - rsort($masks); + + // calculate new table contents + $rows_new = _menu_router_build_calc_rows($menu); + + // compare with old table contents + if (!is_object($changes)) { + $changes = new stdClass; + } + $rows_old = _menu_router_build_load_rows(); + _menu_router_build_calc_changes($rows_old, $rows_new, $changes); + + // save changes + _menu_router_build_save_insert($changes->insert); + _menu_router_build_save_update($changes->update); + _menu_router_build_save_delete($changes->delete); + + // save the masks variable_set('menu_masks', $masks); - cache_set('router:', $menu, 'cache_menu'); + return $menu; } + + /** * Returns TRUE if a path is external (e.g. http://example.com). */ --- menu.rebuild_links.inc +++ menu.rebuild_links.inc @@ -0,0 +1,416 @@ + $item) { + if ($item['_visible']) { + $item = _menu_links_build_build_item($item); + $menu_links[$path] = $item; + $sort[$path] = $item['_number_parts']; + $link_paths[] = $item['link_path']; + } + } + // simulating OOP is ugly... + $cache = _menu_links_build_load_rows($link_paths); + $cache->menu = $menu; + $parent_update_buffer = array(); + if ($menu_links) { + // Make sure no child comes before its parent. + array_multisort($sort, SORT_NUMERIC, $menu_links); + + foreach ($menu_links as $path => $item) { + _menu_links_build_save_item($item, $cache, $parent_update_buffer); + // $sql = "SELECT mlid, menu_name, plid, customized, has_children, updated FROM {menu_links} WHERE link_path = '%s' AND module = '%s'"; + } + } + + // update has_children column + _menu_links_build_multi_update_parental_status($parent_update_buffer, $cache); + + // Updated and customized items whose router paths are gone need new ones. + $router_paths = array_keys($menu); + _menu_links_build_add_missing_router_paths($router_paths); + + // Find any item whose router path does not exist any more. + _menu_links_build_remove_orphan_items($router_paths); +} + + +function _menu_links_build_add_missing_router_paths($paths) { + $placeholders = db_placeholders($paths, 'varchar'); + $sql = "SELECT ml.link_path, ml.mlid, ml.router_path, ml.updated FROM {menu_links} ml WHERE ml.updated = 1 OR (router_path NOT IN ($placeholders) AND external = 0 AND customized = 1)"; + $result = db_query($sql, $paths); + while ($item = db_fetch_array($result)) { + $router_path = _menu_find_router_path($item['link_path']); + if (!empty($router_path) && ($router_path != $item['router_path'] || $item['updated'])) { + // If the router path and the link path matches, it's surely a working + // item, so we clear the updated flag. + $updated = $item['updated'] && $router_path != $item['link_path']; + db_query("UPDATE {menu_links} SET router_path = '%s', updated = %d WHERE mlid = %d", $router_path, $updated, $item['mlid']); + } + } +} + + +function _menu_links_build_remove_orphan_items($paths) { + $placeholders = db_placeholders($paths, 'varchar'); + $sql = "SELECT * FROM {menu_links} WHERE router_path NOT IN ($placeholders) AND external = 0 AND updated = 0 AND customized = 0 ORDER BY depth DESC"; + $result = db_query($sql, $paths); + // Remove all such items. Starting from those with the greatest depth will + // minimize the amount of re-parenting done by menu_link_delete(). + while ($item = db_fetch_array($result)) { + _menu_delete_item($item, TRUE); + } +} + + +/** + * Update parental status for a bunch of parent menu links + * + * @param $parent_update_buffer + * @param $cache + * @return unknown_type + */ +function _menu_links_build_multi_update_parental_status($parent_update_buffer, $cache) { + foreach ($parent_update_buffer as $plid => $children) { + $has_children = false; + foreach ($children as $mlid => $hidden) { + if (!$hidden) { + $has_children = true; + break; + } + } + if ($has_children) { + if (!$cache->rows[$plid]['has_children']) { + db_query("UPDATE {menu_links} SET has_children = %d WHERE mlid = %d", 1, $plid); + $cache->rows[$plid]['has_children'] = 1; + } + } else { + // leave as is + // TODO: can it ever happen that children are removed? + } + } +} + + +/** + * Builds a link from a router item. + */ +function _menu_links_build_build_item($item) { + if ($item['type'] == MENU_CALLBACK) { + $item['hidden'] = -1; + } + elseif ($item['type'] == MENU_SUGGESTED_ITEM) { + $item['hidden'] = 1; + } + // 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', + 'link_title' => $item['title'], + 'link_path' => $item['path'], + 'hidden' => 0, + 'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])), + ); + return $item; +} + + +/** + * Save a menu link. + * + * @param $item array + * An array representing a menu link item. + * drupal_alter has already run. + * @param $existing_item array + * Existing menu link with same mlid, + * or FALSE if an existing item does not exist. + * @return + * The mlid of the saved menu link, or FALSE if the menu link could not be + * saved. + */ +function _menu_links_build_save_item(&$item, $cache, &$parent_update_buffer) { + $existing_item = _menu_links_build_find_existing($item['link_path'], $cache); + 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']) { + return; + } + + drupal_alter('menu_link', $item, $menu); + + $parent = _menu_links_build_find_parent($item, $cache); + + // This is the easiest way to handle the unique internal path '', + // since a path marked as external does not need to match a router path. + $item['_external'] = menu_path_is_external($item['link_path']) || $item['link_path'] == ''; + // Load defaults. + $item += array( + 'menu_name' => 'navigation', + 'weight' => 0, + 'link_title' => '', + 'hidden' => 0, + 'has_children' => 0, + 'expanded' => 0, + 'options' => array(), + 'module' => 'menu', + 'customized' => 0, + 'updated' => 0, + ); + if (is_array($parent)) { + $item['menu_name'] = $parent['menu_name']; + } + // Menu callbacks need to be in the links table for breadcrumbs, but can't + // be parents if they are generated directly from a router item. + if (empty($parent['mlid']) || $parent['hidden'] < 0) { + $item['plid'] = 0; + } else { + $item['plid'] = $parent['mlid']; + } + + $menu_name = $item['menu_name']; + + if (!$existing_item) { + // insert a row + $row = _menu_links_build_calc_row_from_item($item); + $item['mlid'] = _menu_links_build_db_insert_row($row); + $row['mlid'] = $item['mlid']; + $cache->mlids[$item['link_path']][] = $item['mlid']; + $cache->rows[$item['mlid']] = $row; + } + + if (!$item['plid']) { + $item['p1'] = $item['mlid']; + for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) { + $item["p$i"] = 0; + } + $item['depth'] = 1; + } else { + // Cannot add beyond the maximum depth. + if ($item['has_children'] && $existing_item) { + $limit = MENU_MAX_DEPTH - menu_link_children_relative_depth($existing_item) - 1; + } else { + $limit = MENU_MAX_DEPTH - 1; + } + if ($parent['depth'] > $limit) { + return FALSE; + } + $item['depth'] = $parent['depth'] + 1; + _menu_link_parents_set($item, $parent); + } + + // move_children is never necessary, fortunately!! + + // Find the callback. During the menu update we store empty paths to be + // fixed later, so we skip this. + if (!isset($_SESSION['system_update_6021']) && ( + empty($item['router_path']) || + !$existing_item || + ($existing_item['link_path'] != $item['link_path']) + )) { + if ($item['_external']) { + $item['router_path'] = ''; + } else { + // Find the router path which will serve this path. + $item['parts'] = explode('/', $item['link_path'], MENU_MAX_PARTS); + $item['router_path'] = _menu_links_build_find_router_path($item['link_path'], $cache); + } + } + // update row + $row = _menu_links_build_calc_row_from_item($item); + if ($row != $cache->rows[$row['mlid']]) { + // only update if something changed + _menu_links_build_db_update_row($row); + $cache->rows[$row['mlid']] = $row; + } + + // Check the has_children status of the parent. + // If plid == 0, there is nothing to update. + if ($item['plid']) { + $parent_update_buffer[$item['plid']][$item['mlid']] = $item['hidden']; + } + menu_cache_clear($menu_name); + if ($existing_item && $menu_name != $existing_item['menu_name']) { + menu_cache_clear($existing_item['menu_name']); + } + + _menu_clear_page_cache(); + return $item['mlid']; +} + + +/** + * Find an existing menu item to the given $link_path + * + * @param $link_path + * @param $cache + * @return unknown_type + */ +function _menu_links_build_find_existing($link_path, $cache) { + if (isset($cache->mlids[$link_path])) { + $mlid = end($cache->mlids[$link_path]); + return $cache->rows[$mlid]; + } + return FALSE; +} + + +/** + * Calculate a database row for a given item + * + * @param $item + * @return unknown_type + */ +function _menu_links_build_calc_row_from_item($item) { + $row = $item; + $row['options'] = serialize($item['options']); + $row['external'] = $item['_external']; + unset($row['_external']); + return $row; +} + + +function _menu_links_build_db_insert_row($row) { + $fields = array_keys($row); + $fields = implode(', ', $fields); + $placeholders = db_placeholders($args); + $args = array_values($row); + db_query("INSERT INTO {menu_links} ($fields) ($placeholders)", $args); + return db_last_insert_id('menu_links', 'mlid'); +} + + +function _menu_links_build_db_update_row($row) { + $mlid = $row['mlid']; + unset($row['mlid']); + foreach ($row as $k => $v) { + if (is_numeric($v)) { + $set[] = "$k = '%s'"; + } else { + $set[] = "$k = %s"; + } + $args[] = $v; + } + $set = implode(', ', $set); + $args[] = $mlid; + db_query("UPDATE {menu_links} SET $set WHERE mlid = %s", $args); +} + + + +/** + * Find a parent menu item based on $plid and/or $link_path + * + * @param $item + * @return array parent menu item + */ +function _menu_links_build_find_parent($item, $cache) { + if ($item['plid']) { + $plid = $item['plid']; + if (isset($cache->rows[$plid])) { + return $cache->rows[$plid]; + } else { + $row = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $plid)); + $cache->rows[$plid] = $row; + } + } + $parent_path = $item['link_path']; + do { + $parent_path = substr($parent_path, 0, strrpos($parent_path, '/')); + if (!$parent_path) { + break; + } + if (isset($cache->mlids[$parent_path])) { + $mlids = $cache->mlids[$parent_path]; + if (count($mlids == 1)) { + return $cache->rows[end($mlids)]; + } + } + } while (TRUE); + return FALSE; +} + + +/** + * Load all the rows that are relevant to the update. + * + * @return array of rows + */ +function _menu_links_build_load_rows($link_paths) { + $prefix_lookup = array(); + foreach ($link_paths as $path) { + while ($path) { + $prefix_lookup[$path] = true; + $path = substr($path, 0, strrpos($parent_path, '/')); + } + } + $cache = new stdClass; + $cache->rows = array(); + $cache->mlids = array(); + $children = array(); + $result = db_query("SELECT * FROM {menu_links} WHERE module = 'system' ORDER BY depth DESC"); + while ($row = db_fetch_array($result)) { + if (array_key_exists($row['link_path'], $prefix_lookup) || array_key_exists($children[$row['mlid']])) { + $cache->rows[$row['mlid']] = $row; + $cache->mlids[$row['link_path']][] = $row['mlid']; + if ($row['plid']) { + $children[$row['plid']][] = $row['mlid']; + } + } + } + return $cache; +} + + + +/** + * Find the router path which will serve this path. + * + * @param $link_path + * The path for we are looking up its router path. + * @return + * A path from $menu keys or empty if $link_path points to a nonexisting + * place. + */ +function _menu_links_build_find_router_path($link_path, $cache) { + + $router_path = $link_path; + $parts = explode('/', $link_path, MENU_MAX_PARTS); + list($ancestors, $placeholders) = menu_get_ancestors($parts); + + if (!isset($cache->menu[$router_path])) { + // Add an empty path as a fallback. + $ancestors[] = ''; + foreach ($ancestors as $key => $router_path) { + if (isset($cache->menu[$router_path])) { + // Exit the loop leaving $router_path as the first match. + break; + } + } + // If we did not find the path, $router_path will be the empty string + // at the end of $ancestors. + } + return $router_path; +} + --- menu.rebuild_router.inc +++ menu.rebuild_router.inc @@ -0,0 +1,351 @@ + $v) { + $item = &$menu[$path]; + if (!$item['_tab']) { + // Non-tab items. + $item['tab_parent'] = ''; + $item['tab_root'] = $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 (!isset($item['tab_parent'])) { + // Parent stores the parent of the path. + $item['tab_parent'] = $parent_path; + } + if (!isset($item['tab_root']) && !$parent['_tab']) { + $item['tab_root'] = $parent_path; + } + // If an access callback is not found for a default local task we use + // the callback from the parent, since we expect them to be identical. + // In all other cases, the access parameters must be specified. + if (($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access callback']) && isset($parent['access callback'])) { + $item['access callback'] = $parent['access callback']; + if (!isset($item['access arguments']) && isset($parent['access arguments'])) { + $item['access arguments'] = $parent['access arguments']; + } + } + // Same 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['file']) && isset($parent['file'])) { + $item['file'] = $parent['file']; + } + if (!isset($item['file path']) && isset($parent['file path'])) { + $item['file path'] = $parent['file path']; + } + } + } + } + if (!isset($item['access callback']) && isset($item['access arguments'])) { + // Default callback. + $item['access callback'] = 'user_access'; + } + if (!isset($item['access callback']) || empty($item['page callback'])) { + $item['access callback'] = 0; + } + if (is_bool($item['access callback'])) { + $item['access callback'] = intval($item['access callback']); + } + + $item += array( + 'access arguments' => array(), + 'access callback' => '', + 'page arguments' => array(), + 'page callback' => '', + 'block callback' => '', + 'title arguments' => array(), + 'title callback' => 't', + 'description' => '', + 'position' => '', + 'tab_parent' => '', + 'tab_root' => $path, + 'path' => $path, + 'file' => '', + 'file path' => '', + 'include file' => '', + ); + + // Calculate out the file to be included for each callback, if any. + if ($item['file']) { + $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']) : ''; + $rows[$path] = array( + 'path' => $path, + 'load_functions' => $item['load_functions'], + 'to_arg_functions' => $item['to_arg_functions'], + 'access_callback' => $item['access callback'], + 'access_arguments' => serialize($item['access arguments']), + 'page_callback' => $item['page callback'], + 'page_arguments' => serialize($item['page arguments']), + 'fit' => $item['_fit'], + 'number_parts' => $item['_number_parts'], + 'tab_parent' => $item['tab_parent'], + 'tab_root' => $item['tab_root'], + 'title' => $item['title'], + 'title_callback' => $item['title callback'], + 'title_arguments' => $item['title arguments'] ? serialize($item['title arguments']) : '', + 'type' => $item['type'], + 'block_callback' => $item['block callback'], + 'description' => $item['description'], + 'position' => $item['position'], + 'weight' => $item['weight'], + 'file' => $item['include file'], + ); + } + + return $rows; +} + + +/** + * Get callbacks from hook_menu implementations + * + * @return array callbacks + */ +function _menu_router_build_calc_callbacks() { + // We need to manually call each module so that we can know which module + // a given item came from. + $callbacks = array(); + foreach (module_implements('menu') as $module) { + $router_items = call_user_func($module .'_menu'); + if (isset($router_items) && is_array($router_items)) { + foreach (array_keys($router_items) as $path) { + $router_items[$path]['module'] = $module; + } + $callbacks = array_merge($callbacks, $router_items); + } + } + // Alter the menu as defined in modules, keys are like user/%user. + drupal_alter('menu', $callbacks); + return $callbacks; +} + +/** + * Calculate the menu from given callbacks + * + * @param $callbacks array collected from hook_menu implementations + * @return array($menu, $masks) + */ +function _menu_router_build_calc_menu($callbacks) { + // First pass: separate callbacks from paths, making paths ready for + // matching. Calculate fitness, and fill some default values. + $menu = array(); + foreach ($callbacks as $path => $item) { + $load_functions = array(); + $to_arg_functions = array(); + $fit = 0; + $move = FALSE; + + $parts = explode('/', $path, MENU_MAX_PARTS); + $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 load and to_arg functions. + foreach ($parts as $k => $part) { + $match = FALSE; + // Look for wildcards in the form allowed to be used in PHP functions, + // because we are using these to construct the load function names. + // See http://php.net/manual/en/language.functions.php for reference. + if (preg_match('/^%(|[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$/', $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; + $match = TRUE; + } + if (function_exists($matches[1] .'_load')) { + $function = $matches[1] .'_load'; + // Create an array of arguments that will be passed to the _load + // function when this menu path is checked, if 'load arguments' + // exists. + $load_functions[$k] = isset($item['load arguments']) ? array($function => $item['load arguments']) : $function; + $match = TRUE; + } + } + } + if ($match) { + $parts[$k] = '%'; + } + else { + $fit |= 1 << ($slashes - $k); + } + } + if ($fit) { + $move = TRUE; + } + else { + // If there is no %, it fits maximally. + $fit = (1 << $number_parts) - 1; + } + $masks[$fit] = 1; + $item['load_functions'] = empty($load_functions) ? '' : serialize($load_functions); + $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions); + $item += array( + 'title' => '', + 'weight' => 0, + 'type' => MENU_NORMAL_ITEM, + '_number_parts' => $number_parts, + '_parts' => $parts, + '_fit' => $fit, + ); + $item += array( + '_visible' => (bool)($item['type'] & MENU_VISIBLE_IN_BREADCRUMB), + '_tab' => (bool)($item['type'] & MENU_IS_LOCAL_TASK), + ); + if ($move) { + $new_path = implode('/', $item['_parts']); + $menu[$new_path] = $item; + $sort[$new_path] = $number_parts; + } + else { + $menu[$path] = $item; + $sort[$path] = $number_parts; + } + } + array_multisort($sort, SORT_NUMERIC, $menu); + + // Sort the masks so they are in order of descending fit. + $masks = array_keys($masks); + rsort($masks); + + return array($menu, $masks); +} + + +/** + * Find the changes to be stored in the menu_router table + * + * @param $rows_old + * @param $rows_new + * @param $changes object to collect $insert, $update, $delete arrays + * @return $changes object + */ +function _menu_router_build_calc_changes($rows_old, $rows_new, $changes = NULL) { + if (!is_object($changes)) { + $changes = new stdClass; + } + $changes->insert = array(); + $changes->update = array(); + $changes->delete = array(); + foreach ($rows_new as $path => $row_new) { + if (!isset($rows_old[$path])) { + $changes->insert[$path] = $row_new; + } else { + $row_old = $rows_old[$path]; + $changes = array(); + foreach ($row_new as $key => $value) { + if ($value != $row_old[$key]) { + $changes[$key] = $value; + } + } + if (!empty($changes)) { + $changes->update[$path] = $changes; + } + unset($rows_old[$path]); + } + } + // delete remaining rows + foreach ($rows_old as $path => $row_old) { + $changes->delete[$path] = $path; + } + return $changes; +} + + +/** + * Load the old contents of menu_router + * + * @return array rows as in db_fetch_array, keyed by $path + */ +function _menu_router_build_load_rows() { + $rows = array(); + $result = db_query("SELECT * FROM {menu_router}"); + while ($row = db_fetch_array($result)) { + $rows[$row['path']] = $row; + } + return $rows; +} + +/** + * Execute INSERT queries + * + * @param $insert array of rows to insert + */ +function _menu_router_build_save_insert($insert) { + // TODO: ON DUPLICATE KEY UPDATE ? + if (count($insert)) { + // the $sql is always the same + $example_row = end($insert); + foreach ($example_row as $k => $v) { + $fields[] = $k; + $tokens[] = "'%s'"; + } + $fields = implode(', ', $fields); + $tokens = implode(', ', $tokens); + $sql = "INSERT INTO {menu_router} ($fields) VALUES ($tokens)"; + // insert the rows + foreach ($insert as $row) { + $args = array_values($row); + array_unshift($args, $sql); + call_user_func_array('db_query', $args); + } + } +} + +/** + * Execute UPDATE queries + * + * @param $update array of rows with modifications + */ +function _menu_router_build_save_update($update) { + foreach ($update as $changes) { + foreach ($changes as $k => $v) { + $set[] = "$k = %s"; + } + $set = implode(', ', $set); + $sql = "UPDATE {menu_router} SET $set"; + $args = array_values($changes); + array_unshift($args, $sql); + call_user_func_array('db_query', $args); + } +} + +/** + * Execute DELETE queries + * + * @param $delete array of paths (PRIMARY KEY) to delete + */ +function _menu_router_build_save_delete($delete) { + $sql = "DELETE FROM {menu_router} WHERE path = '%s'"; + foreach ($delete as $path) { + db_query($sql, $path); + } +} +