Index: admin_menu.api.php =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/admin_menu/admin_menu.api.php,v retrieving revision 1.4 diff -u -p -r1.4 admin_menu.api.php --- admin_menu.api.php 12 Mar 2010 23:05:54 -0000 1.4 +++ admin_menu.api.php 13 Mar 2010 22:08:04 -0000 @@ -7,6 +7,42 @@ */ /** + * Provide expansion arguments for dynamic menu items, i.e. menu paths + * containing one or more %placeholders. + * + * The map items must be keyed by the dynamic path to expand. Each map item may + * have the following properties: + * + * - parent: The parent menu path to link the expanded items to. + * - arguments: An array of argument sets that will be used in the + * expansion. Each set consists of an array of one or more placeholders with + * an array of possible expansions as value. Upon expansion, each argument + * is combined with every other argument from the set (ie., the cartesian + * product of all arguments is created). The expansion values may be empty, + * that is, you don't need to insert logic to skip map items for which no + * values exist, since admin menu will take care of that. + * - hide: (optional) Used to hide another menu item, usually a superfluous + * "List" item. + * + * @see admin_menu.map.inc + */ +function hook_admin_menu_map() { + // Expand content types in Structure >> Content types. + // The key denotes the dynamic path to expand to multiple menu items. + $map['admin/structure/types/manage/%node_type'] = array( + // Link generated items directly to the "Content types" item, and hide the + // "List" item. + 'parent' => 'admin/structure/types', + 'hide' => 'admin/structure/types/list', + // Create expansion arguments for the %node_type placeholder. + 'arguments' => array( + array('%node_type' => array_keys(node_type_get_types())), + ), + ); + return $map; +} + +/** * Alter content in Administration menu bar before it is rendered. * * @param $content Index: admin_menu.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/admin_menu/admin_menu.inc,v retrieving revision 1.74 diff -u -p -r1.74 admin_menu.inc --- admin_menu.inc 20 Feb 2010 01:02:54 -0000 1.74 +++ admin_menu.inc 14 Mar 2010 00:19:30 -0000 @@ -7,6 +7,337 @@ */ /** + * Build the full administration menu tree from static and expanded dynamic items. + * + * @param $menu_name + * The menu name to use as base for the tree. + */ +function admin_menu_tree($menu_name) { + // Get placeholder expansion arguments from hook_admin_menu_map() + // implementations. + module_load_include('inc', 'admin_menu', 'admin_menu.map'); + $expand_map = module_invoke_all('admin_menu_map'); + // Allow modules to alter the expansion map. + drupal_alter('admin_menu_map', $expand_map); + + $new_map = array(); + $hidden = array(); + foreach ($expand_map as $path => $data) { + // Convert named placeholders, since the menu system stores paths using + // anonymous placeholders. + $replacements = array_fill_keys(array_keys($data['arguments'][0]), '%'); + $data['parent'] = strtr($data['parent'], $replacements); + $new_map[strtr($path, $replacements)] = $data; + + // Collect paths to hide. + if (isset($data['hide'])) { + $hidden[strtr($data['hide'], $replacements)] = 1; + } + } + $expand_map = $new_map; + dsm($expand_map); + unset($new_map); + + // Get static and dynamic local tasks. + $local_tasks = admin_menu_local_tasks($menu_name, $expand_map); + + // Merge local tasks with static menu tree. + $tree = menu_tree_all_data($menu_name); + admin_menu_merge_tree($tree, $local_tasks, array(), $hidden); + + return $tree; +} + +/** + * Load all local tasks for a given menu name. + * + * @param $menu_name + * A menu name to load local tasks for. + * @param $expand_map + * Placeholder expansion arguments. + * + * @see hook_admin_menu_map() + */ +function admin_menu_local_tasks($menu_name, array $expand_map) { + $local_tasks = array(); + + // Fetch all parent items. + $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC)); + $query->join('menu_router', 'm', 'ml.router_path = m.path'); + $query + ->fields('ml') + ->fields('m', array_diff(drupal_schema_fields_sql('menu_router'), drupal_schema_fields_sql('menu_links'))) + ->condition('ml.menu_name', $menu_name); + + $parents = array_keys($expand_map); + $router_paths = db_or(); + foreach ($parents as $path) { + $router_paths->condition('ml.router_path', db_like($path) . '%', 'LIKE'); + } + + $result = $query + ->condition($router_paths) + ->orderBy('fit') + ->execute(); + + foreach ($result as $local_task) { + // Store local tasks grouped by parent path for later merging. + // @todo Stuff like text formats is not contained, because they are no tabs. + // The following line leads to a PHP fatal error due to endless recursion. + $parent_path = ($local_task['tab_parent'] ? $local_task['tab_parent'] : $local_task['tab_root']); + $parent_path = $local_task['tab_parent']; + // Load any missing parent menu items. + admin_menu_build_ancestry($local_tasks, $parent_path, $expand_map); + // Assign placeholder expansion arguments. + if (isset($expand_map[$local_task['path']])) { + $local_task['expand_map'] = $expand_map[$local_task['path']]['arguments']; + } + $local_tasks[$parent_path][] = $local_task; + } +// dsm($local_tasks); +// return array(); + + return $local_tasks; +} + +/** + * Walk through the entire menu tree and merge in local tasks. + * + * @param &$tree + * A menu tree structure as returned by menu_tree_all_data(). + * @param $local_tasks + * A local tasks structure as resturned by admin_menu_local_tasks(). + * @param $expand_map + * Placeholder expansion arguments. + * + * @see menu_tree_all_data() + * @see admin_menu_local_tasks() + */ +function admin_menu_merge_tree(array &$tree, array $local_tasks, array $expand_map = array(), array $hidden = array()) { + foreach ($tree as $key => $data) { + $path = $data['link']['router_path']; + + // Recurse into static submenu tree items. + if ($tree[$key]['below']) { + admin_menu_merge_tree($tree[$key]['below'], $local_tasks, $expand_map, $hidden); + } + // Hide item if it has no local tasks. This is done to prevent hiding + // items that have local tasks from different modules assigned, and only + // one is requesting to hide the item. + elseif (isset($hidden[$path]) && !isset($local_tasks[$path])) { + $tree[$key]['link']['access'] = FALSE; + continue; + } + + // Check if there is a local task that has this item's path as parent. + if (isset($local_tasks[$path])) { + foreach ($local_tasks[$path] as $router_item) { + // If the local task has custom placeholder expansion arguments set, + // merge them with parent arguments. + if (isset($router_item['expand_map'])) { + $expand_map = array_merge($expand_map, $router_item['expand_map']); + } + + // Set up path arguments map; depends on whether the item is dynamic + // (contains placeholders) or not. + if (strpos($router_item['path'], '%') === FALSE) { + // Build static item and subtree. + $map = explode('/', $router_item['path']); + $item = admin_menu_translate($router_item, $map); + admin_menu_merge_tree($item, $local_tasks, $expand_map, $hidden); + $tree[$key]['below'] += $item; + } + else { + // Drop dynamic items without expansion parameters. + if (empty($expand_map)) { + continue; + } + + // Expand anonymous to named placeholders. + // @see _menu_load_objects() + // @todo explode() => implode() => explode() required for strtr() + $active = ''; + $path = explode('/', $router_item['path']); + $load_functions = unserialize($router_item['load_functions']); + foreach ($load_functions as $index => $function) { + if ($function) { + if (is_array($function)) { + list($function,) = each($function); + } + // Add the loader function name minus "_load". + $placeholder = '%' . substr($function, 0, -5); + $path[$index] = $placeholder; + } + } + $path = implode('/', $path); + + // Create new menu items using expansion arguments. + foreach ($expand_map as $arguments) { + // Create the cartesian product for all arguments and create new + // menu items for each generated combination thereof. + foreach (admin_menu_expand_args($arguments) as $replacements) { + $map = explode('/', strtr($path, $replacements)); + $item = admin_menu_translate($router_item, $map); + // Build subtree using current replacement arguments. + // @todo Avoid rebuilding this for each item. + $current_map = array(); + foreach ($replacements as $placeholder => $value) { + $current_map[$placeholder] = array($value); + } + admin_menu_merge_tree($item, $local_tasks, array($current_map), $hidden); + $tree[$key]['below'] += $item; + } + } + } + } + // Sort new subtree items. + ksort($tree[$key]['below']); + } + } +} + +/** + * Create a menu item suitable for rendering. + * + * @param $router_item + * A menu router item. + * @param $map + * A path map with placeholders replaced. + */ +function admin_menu_translate($router_item, $map) { + _menu_translate($router_item, $map, TRUE); + + // Run through hook_translated_menu_link_alter() to add devel information, + // if configured. + $router_item['menu_name'] = 'management'; + admin_menu_translated_menu_link_alter($router_item, NULL); + + if ($router_item['access']) { + // Turn local task menu callbacks into regular menu items, otherwise they + // won't be visible. + if ($router_item['type'] == MENU_CALLBACK) { + $router_item['type'] = MENU_NORMAL_ITEM; + } + // Fill in pseudo menu link values for rendering later. + $router_item['router_path'] = $router_item['path']; + $router_item['mlid'] = $router_item['href']; + // @todo Strip potential HTML from titles? + $router_item['title'] = strip_tags($router_item['title']); + + $key = (50000 + $router_item['weight']) . ' ' . $router_item['title'] . ' ' . $router_item['mlid']; + return array($key => array( + 'link' => $router_item, + 'below' => array(), + )); + } + + return array(); +} + +/** + * Create the cartesian product of multiple varying sized argument arrays. + * + * @param $arguments + * A two dimensional array of arguments. + * + * @see hook_admin_menu_map() + */ +function admin_menu_expand_args($arguments) { + $replacements = array(); + + // Initialize line cursors, move out array keys (placeholders) and assign + // numeric keys instead. + $i = 0; + $placeholders = array(); + $new_arguments = array(); + foreach ($arguments as $placeholder => $values) { + // Skip empty arguments. + if (empty($values)) { + continue; + } + $cursor[$i] = 0; + $placeholders[$i] = $placeholder; + $new_arguments[$i] = $values; + $i++; + } + $arguments = $new_arguments; + unset($new_arguments); + + if ($rows = count($arguments)) { + do { + // Collect current argument from each row. + $row = array(); + for ($i = 0; $i < $rows; ++$i) { + $row[$placeholders[$i]] = $arguments[$i][$cursor[$i]]; + } + $replacements[] = $row; + + // Increment cursor position. + $j = $rows - 1; + $cursor[$j]++; + while (!array_key_exists($cursor[$j], $arguments[$j])) { + // No more arguments left: reset cursor, go to next line and increment + // that cursor instead. Repeat until argument found or out of rows. + $cursor[$j] = 0; + if (--$j < 0) { + // We're done. + break 2; + } + $cursor[$j]++; + } + } while (1); + } + + return $replacements; +} + +/** + * Add missing dynamic parent menu items to complete the menu tree ancestry. + * + * @param &$local_tasks + * Static and dynamic local tasks. + * @param $path + * Router path to validate. + * @param $expand_map + * Placeholder expansion arguments. + * + * @see admin_menu_local_tasks() + * @see hook_admin_menu_map() + */ +function admin_menu_build_ancestry(array &$local_tasks, $path, array $expand_map) { + if (!isset($local_tasks[$path])) { + $local_tasks[$path] = array(); + + // Add the parent items if they are dynamic and therefore missing in the + // tree structure built by menu_tree_all_data(). + if (isset($expand_map[$path]) && ($router_item = admin_menu_get_item($path))) { + $parent_path = $expand_map[$path]['parent']; + // If the parent's parent item is also missing, enter recursion. + if (!isset($local_tasks[$parent_path])) { + admin_menu_build_ancestry($local_tasks, $parent_path, $expand_map); + } + // Assign placeholder expansion arguments. + $router_item['expand_map'] = $expand_map[$path]['arguments']; + $local_tasks[$parent_path][] = $router_item; + } + } +} + +/** + * Load a single menu router item. + * + * @see menu_get_item() + */ +function admin_menu_get_item($path) { + return db_select('menu_router') + ->fields('menu_router') + ->condition('path', $path) + ->range(0, 1) + ->addTag('menu_get_item') + ->execute()->fetchAssoc(); +} + +/** * Build the administration menu as renderable menu links. * * @param $tree @@ -22,8 +353,8 @@ function admin_menu_links_menu($tree) { $links = array(); foreach ($tree as $data) { - // Skip menu callbacks (mostly dynamic items). - if (isset($data['link']['type']) && $data['link']['type'] == MENU_CALLBACK) { + // Skip invisible items. + if (!$data['link']['access'] || $data['link']['type'] == MENU_CALLBACK) { continue; } // Hide 'Administer' and make child links appear on this level. Index: admin_menu.install =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/admin_menu/admin_menu.install,v retrieving revision 1.23 diff -u -p -r1.23 admin_menu.install --- admin_menu.install 19 Feb 2010 20:42:10 -0000 1.23 +++ admin_menu.install 13 Mar 2010 21:49:54 -0000 @@ -88,3 +88,16 @@ function admin_menu_update_7302() { ->execute(); } +/** + * Remove local tasks from {menu_links} table. + */ +function admin_menu_update_7303() { + $paths = db_query('SELECT path FROM {menu_router} WHERE path LIKE :prefix AND type & :type', array( + ':prefix' => 'admin/%', + ':type' => MENU_IS_LOCAL_TASK, + ))->fetchCol(); + db_delete('menu_links') + ->condition('router_path', $paths, 'IN') + ->execute(); +} + Index: admin_menu.map.inc =================================================================== RCS file: admin_menu.map.inc diff -N admin_menu.map.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ admin_menu.map.inc 14 Mar 2010 00:21:10 -0000 @@ -0,0 +1,128 @@ +> Content authoring >> Text formats + $map['admin/config/content/formats/%filter_format'] = array( + 'parent' => 'admin/config/content/formats', + 'hide' => 'admin/config/content/formats/list', + 'arguments' => array( + array('%filter_format' => array_keys(filter_formats())), + ), + ); + return $map; +} + +/** + * Implements hook_admin_menu_map() on behalf of Menu module. + */ +function menu_admin_menu_map() { + // Structure >> Menus + $map['admin/structure/menu/manage/%menu'] = array( + 'parent' => 'admin/structure/menu', + 'hide' => 'admin/structure/menu/list', + 'arguments' => array( + array('%menu' => array_keys(menu_get_menus())), + ), + ); + return $map; +} + +/** + * Implements hook_admin_menu_map() on behalf of Node module. + */ +function node_admin_menu_map() { + // Structure >> Content types + $map['admin/structure/types/manage/%node_type'] = array( + 'parent' => 'admin/structure/types', + 'hide' => 'admin/structure/types/list', + 'arguments' => array( + array('%node_type' => array_keys(node_type_get_types())), + ), + ); + return $map; +} + +/** + * Implements hook_admin_menu_map() on behalf of Field UI module. + */ +function field_ui_admin_menu_map() { + foreach (entity_get_info() as $obj_type => $info) { + foreach ($info['bundles'] as $bundle_name => $bundle_info) { + if (isset($bundle_info['admin'])) { + $arguments = array(); + switch ($obj_type) { + case 'node': + // Provide replacement sets for content types, note that the + // fields (%field_ui_menu) are different for each content type. + foreach (array_keys($info['bundles']) as $node_type) { + $fields = array(); + foreach (field_info_instances($obj_type, $node_type) as $field) { + $fields[] = $field['field_name']; + } + $arguments[] = array( + '%node_type' => array($node_type), + '%field_ui_menu' => $fields, + ); + } + break; + + case 'user': + $arguments = array(array('%field_ui_menu' => array_keys(field_info_fields('user')))); + break; + } + if (!empty($arguments)) { + $path = $bundle_info['admin']['path']; + $map["$path/fields/%field_ui_menu"] = array( + 'parent' => "$path/fields", + 'arguments' => $arguments, + ); + } + } + } + } + return $map; +} + +/** + * Implements hook_admin_menu_map() on behalf of Taxonomy module. + */ +function taxonomy_admin_menu_map() { + // Structure >> Menus + $map['admin/structure/taxonomy/%taxonomy_vocabulary'] = array( + 'parent' => 'admin/structure/taxonomy', + 'hide' => 'admin/structure/taxonomy/list', + 'arguments' => array( + array('%taxonomy_vocabulary' => array_keys(taxonomy_get_vocabularies())), + ), + ); + return $map; +} + +/** + * Implements hook_admin_menu_map() on behalf of Views UI module. + */ +function views_ui_admin_menu_map() { + // Structure >> Views + // @todo Requires patch to views_ui. + $map['admin/structure/views/edit/%views_ui_cache'] = array( + 'parent' => 'admin/structure/views', + 'hide' => 'admin/structure/views/list', + 'arguments' => array( + array('%views_ui_cache' => array_keys(views_get_all_views())), + ), + ); + return $map; +} + Index: admin_menu.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/admin_menu/admin_menu.module,v retrieving revision 1.112 diff -u -p -r1.112 admin_menu.module --- admin_menu.module 20 Feb 2010 02:17:55 -0000 1.112 +++ admin_menu.module 13 Mar 2010 21:50:04 -0000 @@ -406,7 +406,7 @@ function admin_menu_output() { module_load_include('inc', 'admin_menu'); // Add administration menu. - $content['menu'] = admin_menu_links_menu(menu_tree_all_data('management')); + $content['menu'] = admin_menu_links_menu(admin_menu_tree('management')); $content['menu']['#theme'] = 'admin_menu_links'; // Ensure the menu tree is rendered between the icon and user links. $content['menu']['#weight'] = 0;