insert); _menu_router_build_save_update($changes->update); _menu_router_build_save_delete($changes->delete); // save the masks variable_set('menu_masks', $masks); return $menu; } /** * Calculate the new content for the menu_router table * * @param $menu array as generated by _menu_router_build_calc_menu() * @return array the rows, as in db_fetch_array(). */ function _menu_router_build_calc_rows(array $menu) { // Apply inheritance rules. $rows_new = array(); 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']) : ''; $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); } } // ========================== in menu.rebuild_links.inc ============ /** * Build menu links for the items in the menu router. * The information in $changes allows a more fine-grained update. * * TODO: Use the info in $changes, update only what has changed * * @param $menu array * @param $changes object with $insert, $update, $delete arrays */ function _menu_links_build_rebuild($menu, $changes = NULL) { // Add normal and suggested items as links. $menu_links = array(); $link_paths = array(); foreach ($menu as $path => $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; }