diff options
Diffstat (limited to 'includes/menu.inc')
-rw-r--r-- | includes/menu.inc | 370 |
1 files changed, 269 insertions, 101 deletions
diff --git a/includes/menu.inc b/includes/menu.inc index fc96195ba..acd0027a0 100644 --- a/includes/menu.inc +++ b/includes/menu.inc @@ -172,6 +172,19 @@ define('MENU_SITE_OFFLINE', 4); */ /** + * @Name Menu operations + * @{ + * Menu helper possible operations. + */ + +define('MENU_HANDLE_REQUEST', 0); +define('MENU_RENDER_LINK', 1); + +/** + * @} End of "Menu helper directions + */ + +/** * Returns the ancestors (and relevant placeholders) for any given path. * * For example, the ancestors of node/12345/edit are: @@ -195,7 +208,7 @@ define('MENU_SITE_OFFLINE', 4); * array('node', '12345', 'edit'). * @return * An array which contains the ancestors and placeholders. Placeholders - * simply contain as many %s as the ancestors. + * simply contain as many '%s' as the ancestors. */ function menu_get_ancestors($parts) { $n1 = count($parts); @@ -273,10 +286,10 @@ function menu_unserialize($data, $map) { * with keys like title, access callback, access arguments etc. */ function menu_set_item($path, $item) { - menu_get_item($path, TRUE, $item); + menu_get_item($path, $item); } -function menu_get_item($path = NULL, $execute = TRUE, $item = NULL) { +function menu_get_item($path = NULL, $item = NULL) { static $items; if (!isset($path)) { $path = $_GET['q']; @@ -289,14 +302,13 @@ function menu_get_item($path = NULL, $execute = TRUE, $item = NULL) { $parts = array_slice($map, 0, 6); list($ancestors, $placeholders) = menu_get_ancestors($parts); if ($item = db_fetch_object(db_query_range('SELECT * FROM {menu} WHERE path IN ('. implode (',', $placeholders) .') ORDER BY fit DESC', $ancestors, 0, 1))) { - $item->access = _menu_access($item, $map); + list($item->access, $map) = _menu_translate($item, $map); if ($map === FALSE) { $items[$path] = FALSE; return FALSE; } - if ($execute) { - $item->page_arguments = array_merge(menu_unserialize($item->page_arguments, $map), array_slice($parts, $item->number_parts)); - } + $item->map = $map; + $item->page_arguments = array_merge(menu_unserialize($item->page_arguments, $map), array_slice($parts, $item->number_parts)); } $items[$path] = $item; } @@ -313,45 +325,113 @@ function menu_execute_active_handler() { return MENU_NOT_FOUND; } -function _menu_access($item, &$map) { - if ($item->map_callback) { - $map = call_user_func_array($item->map_callback, array_merge(array($map), unserialize($item->map_arguments))); - if ($map === FALSE) { - return FALSE; +/** + * Handles dynamic path translation and menu access control. + * + * When a user arrives on a page such as node/5, this function determines + * what "5" corresponds to, by inspecting the page's menu path definition, + * node/%node. This will call node_load(5) to load the corresponding node + * object. + * + * It also works in reverse, to allow the display of tabs and menu items which + * contain these dynamic arguments, translating node/%node to node/5. + * This operation is called MENU_RENDER_LINK. + * + * @param $item + * A menu item object + * @param $map + * An array of path arguments (ex: array('node', '5')) + * @param $operation + * The path translation operation to perform: + * - MENU_HANDLE_REQUEST: An incoming page reqest; map with appropriate callback. + * - MENU_RENDER_LINK: Render an internal path as a link. + * @return + * Returns an array. The first value is the access, the second is the map + * with objects loaded where appropriate and the third is the path ready for + * printing. + */ +function _menu_translate($item, $map, $operation = MENU_HANDLE_REQUEST) { + $path = ''; + + // Check if there are dynamic arguments in the path that need to be calculated. + if ($item->load_functions || ($operation == MENU_RENDER_LINK && $item->to_arg_functions)) { + $load_functions = unserialize($item->load_functions); + $to_arg_functions = unserialize($item->to_arg_functions); + $path_map = ($operation == MENU_HANDLE_REQUEST) ? $map : explode('/', $item->path); + foreach ($load_functions as $index => $load_function) { + // Translate place-holders into real values. + if ($operation == MENU_RENDER_LINK) { + if (isset($to_arg_functions[$index])) { + $to_arg_function = $to_arg_functions[$index]; + $return = $to_arg_function(!empty($map[$index]) ? $map[$index] : ''); + if (!empty($map[$index]) || isset($return)) { + $path_map[$index] = $return; + } + else { + unset($path_map[$index]); + } + } + else { + $path_map[$index] = isset($map[$index]) ? $map[$index] : ''; + } + } + // We now have a real path regardless of operation, map it. + if ($load_function) { + $return = $load_function(isset($path_map[$index]) ? $path_map[$index] : ''); + // If callback returned an error or there is no callback, trigger 404. + if ($return === FALSE) { + return array(FALSE, FALSE, ''); + } + $map[$index] = $return; + } + } + if ($operation != MENU_HANDLE_REQUEST) { + // Re-join the path with the new replacement value. + $path = implode('/', $path_map); } } + else { + $path = $item->path; + } + + // Determine access callback, which will decide whether or not the current user has + // access to this path. $callback = $item->access_callback; + // Check for a TRUE or FALSE value. if (is_numeric($callback)) { - return $callback; + return array($callback, $map, $path); } $arguments = menu_unserialize($item->access_arguments, $map); // As call_user_func_array is quite slow and user_access is a very common // callback, it is worth making a special case for it. if ($callback == 'user_access') { - return (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]); + $access = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]); + return array($access, $map, $path); } - return call_user_func_array($callback, $arguments); + return array(call_user_func_array($callback, $arguments), $map, $path); } /** * Returns a rendered menu tree. */ function menu_tree() { - $item = menu_get_item(); - list(, $menu) = _menu_tree(db_query('SELECT * FROM {menu} WHERE pid IN ('. $item->parents .') AND visible = 1 ORDER BY vancode')); - return $menu; + if ($item = menu_get_item()) { + list(, $menu) = _menu_tree(db_query('SELECT * FROM {menu} WHERE pid IN ('. $item->parents .') AND visible = 1 ORDER BY vancode')); + return $menu; + } } function _menu_tree($result = NULL, $depth = 0, $link = array('link' => '', 'has_children' => FALSE)) { static $original_map; $remnant = array('link' => '', 'has_children' => FALSE); $tree = ''; + $map = arg(NULL); while ($item = db_fetch_object($result)) { - $map = arg(NULL, $item->path); - if (!_menu_access($item, $map)) { + list($access, , $path) = _menu_translate($item, $map, MENU_RENDER_LINK); + if (!$access) { continue; } - $menu_link = array('link' => $item->menu_link, 'has_children' => $item->has_children); + $menu_link = array('link' => l($item->title, $path), 'has_children' => $item->has_children); if ($item->depth > $depth) { list($remnant, $menu) = _menu_tree($result, $item->depth, $menu_link); $tree .= theme('menu_tree', $link, $menu); @@ -385,7 +465,11 @@ function theme_menu_tree($link, $tree) { * Generate the HTML for a menu link. */ function theme_menu_link($link, $menu = '') { - return '<li class="'. ($menu ? 'expanded' : ($link['has_children'] ? 'collapsed' : 'leaf')) .'">'. $link['link'] . $menu .'</li>' . "\n"; + return '<li class="'. ($menu ? 'expanded' : (empty($link['has_children']) ? 'leaf': 'collapsed')) .'">'. $link['link'] . $menu .'</li>' . "\n"; +} + +function theme_menu_local_task($link, $active = FALSE) { + return '<li '. ($active ? 'class="active" ' : ''). '>'. $link .'</li>'; } /** @@ -430,47 +514,96 @@ function menu_rebuild() { $function($menu); } $mid = 1; - // First pass. + // First pass: separate callbacks from pathes, making pathes ready for + // matching. Calculate fitness, and fill some default values. foreach ($menu as $path => $item) { - $item = &$menu[$path]; $parts = explode('/', $path, 6); $number_parts = count($parts); - // We store the highest index of parts here to save some work in the weight + // We store the highest index of parts here to save some work in the fit // calculation loop. $slashes = $number_parts - 1; + $fit = 0; + $load_functions = array(); + $to_arg_functions = array(); + // extract functions + foreach ($parts as $k => $part) { + $match = FALSE; + if (preg_match('/^%([a-z_]*)$/', $part, $matches)) { + if (empty($matches[1])) { + $match = TRUE; + } + else { + if (function_exists($matches[1] .'_load')) { + $load_functions[$k] = $matches[1] .'_load'; + $match = TRUE; + } + if (function_exists($matches[1] .'_to_arg')) { + $to_arg_functions[$k] = $matches[1].'_to_arg'; + $match = TRUE; + } + if (!isset($load_functions[$k]) && isset($to_arg_functions[$k])) { + $load_functions[$k] = FALSE; + } + } + } + if ($match) { + $parts[$k] = '%'; + } + else { + $fit |= 1 << ($slashes - $k); + } + } + $item['load_functions'] = empty($load_functions) ? '' : serialize($load_functions); + $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions); // If there is no %, it fits maximally. - if (strpos($path, '%') === FALSE) { + if (!$fit) { $fit = (1 << $number_parts) - 1; + $move = FALSE; } else { - // We need to calculate the fitness. - $fit = 0; - foreach ($parts as $k => $part) { - // ($part != '%') is the bit we want and we shift it to its place - // by shifting to left by ($slashes - $k) bits. - $fit |= ($part != '%') << ($slashes - $k); - } + $move = TRUE; } - if (!isset($item['_visible'])) { - $item['_visible'] = (!isset($item['type']) || ($item['type'] & MENU_VISIBLE_IN_TREE)) ? 1 : 0; + $item += array( + 'title' => '', + 'weight' => 0, + 'type' => MENU_NORMAL_ITEM, + '_number_parts' => $number_parts, + '_parts' => $parts, + '_fit' => $fit, + '_mid' => $mid++, + ); + $item += array( + '_visible' => (bool)($item['type'] & MENU_VISIBLE_IN_TREE), + '_tab' => (bool)($item['type'] & MENU_IS_LOCAL_TASK), + ); + if ($move) { + $new_path = implode('/', $item['_parts']); + unset($menu[$path]); } - $depth = 1; - if (!isset($item['_mid'])) { - $item['_mid'] = $mid++; + else { + $new_path = $path; } + $menu[$new_path] = $item; + } + // Second pass: find visible parents and prepare for sorting. + foreach ($menu as $path => $item) { + $item = &$menu[$path]; + $number_parts = $item['_number_parts']; $parents = array($item['_mid']); + if ($item['_visible'] && isset($item['parent'])) { + $parent_parts = explode('/', $item['parent'], 6); + $slashes = count($parent_parts) - 1; + } + else { + $parent_parts = $item['_parts']; + $slashes = $number_parts -1; + } + $depth = 1; for ($i = $slashes; $i; $i--) { - $parent_path = implode('/', array_slice($parts, 0, $i)); + $parent_path = implode('/', array_slice($parent_parts, 0, $i)); // We need to calculate depth to be able to sort. depth needs visibility. if (isset($menu[$parent_path])) { $parent = &$menu[$parent_path]; - // It's possible that the parent was not processed yet. - if (!isset($parent['_mid'])) { - $parent['_mid'] = $mid++; - } - if (!isset($parent['_visible'])) { - $parent['_visible'] = (!isset($parent['type']) || ($parent['type'] & MENU_VISIBLE_IN_TREE)) ? 1 : 0; - } if ($item['_visible'] && $parent['_visible']) { $parent['_has_children'] = 1; $depth++; @@ -487,22 +620,16 @@ function menu_rebuild() { $parents = implode(',', array_reverse($parents)); // Store variables and set defaults. $item += array( - '_fit' => $fit, - '_number_parts' => $number_parts, - '_parts' => $parts, '_pid' => 0, - '_depth' => $depth, + '_depth' => $item['_visible'] ? $depth : $number_parts, '_parents' => $parents, '_has_children' => 0, - 'title' => '', - 'weight' => 0, - 'type' => MENU_NORMAL_ITEM, ); - $sort[$path] = ($item['_visible'] ? $depth : $number_parts) . sprintf('%05d', $item['weight']) . $item['title']; + $sort[$path] = $item['_depth'] . sprintf('%05d', $item['weight']) . $item['title']; unset($item); } array_multisort($sort, $menu); - // Second pass: calculate ancestors, vancode and store into the database. + // Third pass: calculate ancestors, vancode and store into the database. foreach ($menu as $path => $item) { $item = &$menu[$path]; for ($i = $item['_number_parts'] - 1; $i; $i--) { @@ -512,7 +639,7 @@ function menu_rebuild() { // If a callback is not found, we try to find the first parent that // has this callback. When found, its callback argument will also be // copied but only if there is none in the current item. - foreach (array('access', 'map', 'page') as $type) { + foreach (array('access', 'page') as $type) { if (!isset($item["$type callback"]) && isset($parent["$type callback"])) { $item["$type callback"] = $parent["$type callback"]; if (!isset($item["$type arguments"]) && isset($parent["$type arguments"])) { @@ -525,9 +652,6 @@ function menu_rebuild() { if (!isset($item['access callback'])) { $menu[$path]['access callback'] = isset($item['access arguments']) ? 'user_access' : 0; } - if (!isset($item['map callback']) && isset($item['map arguments'])) { - $item['map callback'] = 'menu_map'; - } if (is_bool($item['access callback'])) { $item['access callback'] = intval($item['access callback']); } @@ -538,42 +662,39 @@ function menu_rebuild() { } $vancode = $prefix . int2vancode($next[$prefix]++); $menu[$path]['_prefix'] = $vancode .'.'; - $link = l($item['title'], $path, isset($item['attributes']) ? $item['attributes'] : array(), isset($item['query']) ? $item['query'] : NULL, isset($item['fragment']) ? $item['fragment'] : NULL); } else { $vancode = ''; - $link = ''; } - $tab = ($item['type'] & MENU_IS_LOCAL_TASK) ? 1 : 0; - $default_tab = $item['type'] == MENU_DEFAULT_LOCAL_TASK; - if (!isset($item['parent'])) { - if ($tab) { + if ($item['_tab']) { + if (!isset($item['parent'])) { $item['parent'] = implode('/', array_slice($item['_parts'], 0, $item['_number_parts'] - 1)); } - else { - $item['parent'] = $path; - } + } + else { + // Non-tab items specified the parent for visible links, and it's + // stored in parents, parent stores the tab parent. + $item['parent'] = $path; } $insert_item = $item + array( 'access arguments' => array(), 'access callback' => '', 'page arguments' => array(), 'page callback' => '', - 'map arguments' => array(), - 'map callback' => '', ); db_query("INSERT INTO {menu} ( - mid, pid, path, - access_callback, access_arguments, page_callback, page_arguments, map_callback, map_arguments, fit, - number_parts, vancode, menu_link, visible, parents, depth, has_children, tab, default_tab, title, parent) - VALUES (%d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', %d, '%s', %d, %d, %d, %d, '%s', '%s')", - $insert_item['_mid'], $insert_item['_pid'], $path, $insert_item['access callback'], - serialize($insert_item['access arguments']), $insert_item['page callback'], - serialize($insert_item['page arguments']), $insert_item['map callback'], - serialize($insert_item['map arguments']), $insert_item['_fit'], - $insert_item['_number_parts'], $vancode .'+', $link, $insert_item['_visible'], - $insert_item['_parents'], $insert_item['_depth'], $insert_item['_has_children'], - $tab, $default_tab, $insert_item['title'], $insert_item['parent']); + mid, pid, path, load_functions, to_arg_functions, + access_callback, access_arguments, page_callback, page_arguments, fit, + number_parts, vancode, visible, parents, depth, has_children, tab, title, parent, type) + VALUES (%d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', %d, '%s', %d, %d, %d, '%s', '%s', '%s')", + $insert_item['_mid'], $insert_item['_pid'], $path, + $insert_item['load_functions'], $insert_item['to_arg_functions'], + $insert_item['access callback'], serialize($insert_item['access arguments']), + $insert_item['page callback'], serialize($insert_item['page arguments']), + $insert_item['_fit'], $insert_item['_number_parts'], $vancode .'+', + $insert_item['_visible'], $insert_item['_parents'], $insert_item['_depth'], + $insert_item['_has_children'], $item['_tab'], $insert_item['title'], + $insert_item['parent'], $insert_item['type']); unset($item); } } @@ -590,32 +711,79 @@ function menu_primary_links() { function menu_secondary_links() { } -function menu_primary_local_tasks() { - $router_item = menu_get_item(); - $result = db_query("SELECT * FROM {menu} WHERE parent = '%s' AND tab = 1 ORDER BY vancode", $router_item->parent); - $tabs = array(); - while ($item = db_fetch_object($result)) { - $map = explode('/', $item->path); - foreach ($map as $key => $value) { - if ($value == '%') { - $map[$key] = arg($key); +/** + * Collects the local tasks (tabs) for a given level. + * + * @param $level + The level of tasks you ask for. Primary tasks are 0, secondary are 1... + * @return + * An array of links to the tabs. + */ +function menu_local_tasks($level = 0) { + static $tabs = array(), $parents = array(), $parents_done = array(); + if (empty($tabs)) { + $router_item = menu_get_item(); + $map = arg(NULL); + do { + // Tabs are router items that have the same parent. If there is a new + // parent, let's add it the queue. + if (!empty($router_item->parent)) { + $parents[] = $router_item->parent; + // Do not add the same item twice. + $router_item->parent = ''; } - } - $path = implode('/', $map); - if (_menu_access($item, $map, TRUE)) { - $link = l($item->title, $path); - if ((!$router_item->tab && $item->default_tab) || ($path == $_GET['q'])) { - $tabs[] = array('class' => 'active', 'data' => $link); + $parent = array_shift($parents); + // Do not process the same parent twice. + if (isset($parents_done[$parent])) { + continue; } - else { - $tabs[] = $link; + // This loads all the tabs. + $result = db_query("SELECT * FROM {menu} WHERE parent = '%s' AND tab = 1 ORDER BY vancode", $parent); + $tabs_current = ''; + while ($item = db_fetch_object($result)) { + // This call changes the path from for example user/% to user/123 and + // also determines whether we are allowed to access it. + list($access, , $path) = _menu_translate($item, $map, MENU_RENDER_LINK); + if ($access) { + $depth = $item->depth; + $link = l($item->title, $path); + // We check for the active tab. + if ($item->path == $router_item->path || (!$router_item->tab && $item->type == MENU_DEFAULT_LOCAL_TASK) || $path == $_GET['q']) { + $tabs_current .= theme('menu_local_task', $link, TRUE); + // Let's try to find the router item one level up. + $next_router_item = db_fetch_object(db_query("SELECT path, tab, parent FROM {menu} WHERE path = '%s'", $item->parent)); + // We will need to inspect one level down. + $parents[] = $item->path; + } + else { + $tabs_current .= theme('menu_local_task', $link); + } + } } - } + // If there are tabs, let's add them + if ($tabs_current) { + $tabs[$depth] = $tabs_current; + } + $parents_done[$parent] = TRUE; + if (isset($next_router_item)) { + $router_item = $next_router_item; + } + unset($next_router_item); + } while ($parents); + // Sort by depth + ksort($tabs); + // Remove the depth, we are interested only in their relative placement. + $tabs = array_values($tabs); } - return theme('item_list', $tabs, NULL, 'ul', array('class' => 'tabs primary')); + return isset($tabs[$level]) ? $tabs[$level] : array(); +} + +function menu_primary_local_tasks() { + return menu_local_tasks(); } function menu_secondary_local_tasks() { + return menu_local_tasks(1); } function menu_set_active_item() { @@ -631,4 +799,4 @@ function menu_get_active_breadcrumb() { function menu_get_active_title() { $item = menu_get_item(); return $item->title; -}
\ No newline at end of file +} |