Index: admin_menu.api.php =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/admin_menu/admin_menu.api.php,v retrieving revision 1.3 diff -u -p -r1.3 admin_menu.api.php --- admin_menu.api.php 22 Aug 2009 15:31:11 -0000 1.3 +++ admin_menu.api.php 12 Nov 2009 04:12:44 -0000 @@ -7,6 +7,44 @@ */ /** + * 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": Required. The parent menu path to link the expanded items to. + * - "hide": Optional. Used to hide another menu item, usually a superfluous + * "List" item. + * - "arguments": Requires. 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. + * + * admin_menu_admin_menu_expand_map() provides an example of a map item that + * uses more than argument set: for each content type a list of type specific + * fields needs to be generated as secondary argument. + */ +function hook_admin_menu_expand_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.68 diff -u -p -r1.68 admin_menu.inc --- admin_menu.inc 4 Nov 2009 21:51:19 -0000 1.68 +++ admin_menu.inc 12 Nov 2009 05:29:08 -0000 @@ -7,6 +7,328 @@ */ /** + * Build the full admin menu tree from static and expanded dynamic items. + * + * @param $menu_name + * Menu name to use as base for the tree. + */ +function admin_menu_tree($menu_name) { + // Get placeholder expansion arguments from hook_admin_menu_expand_map() + // implementations. + module_load_include('inc', 'admin_menu', 'admin_menu.map'); + $expand_map = module_invoke_all('admin_menu_expand_map'); + drupal_alter('admin_menu_expand_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; + 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; +} + +/** + * 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']; + + if ($tree[$key]['below']) { + // Recurse into static submenu tree items. + admin_menu_merge_tree($tree[$key]['below'], $local_tasks, $expand_map, $hidden); + } + elseif (isset($hidden[$path]) && !isset($local_tasks[$path])) { + // 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. + $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']); + return array((50000 + $router_item['weight']) . ' ' . $router_item['title'] . ' ' . $router_item['mlid'] => 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_expand_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; +} + +/** + * 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_expand_map() + */ +function admin_menu_local_tasks($menu_name, array $expand_map) { + $local_tasks = array(); + + // Fetch all root level items. + $parents = db_select('menu_links') + ->fields('menu_links', array('router_path')) + ->condition('menu_name', $menu_name) + ->condition('depth', 1) + ->condition('hidden', 0) + ->condition('external', 0) + ->execute()->fetchAll(PDO::FETCH_ASSOC); + + if ($parents) { + foreach ($parents as $parent) { + // Fetch all local tasks that are chil elements of the parent item. + $result = db_select('menu_router', NULL, array('fetch' => PDO::FETCH_ASSOC)) + ->fields('menu_router') + ->where("LEFT(tab_root, " . (strlen($parent['router_path']) + 1) . ") = :router_path", array(':router_path' => $parent['router_path'] . '/')) + ->condition('type', MENU_LOCAL_TASK | MENU_LOCAL_ACTION, '&') + ->condition('context', MENU_CONTEXT_INLINE, '<>') + ->orderBy('fit') + ->execute(); + + foreach ($result as $local_task) { + // Store local tasks grouped by parent path for later merging. + $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; + } + } + } + + return $local_tasks; +} + +/** + * 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_expand_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 (!isset($local_tasks[$parent_path])) { + // Enter recursion to create the parent's parent item if it is also + // missing. + 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 +344,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.22 diff -u -p -r1.22 admin_menu.install --- admin_menu.install 9 Sep 2009 22:51:55 -0000 1.22 +++ admin_menu.install 12 Nov 2009 04:44:35 -0000 @@ -93,3 +93,16 @@ function admin_menu_update_7302() { return $ret; } +/** + * 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 12 Nov 2009 05:28:04 -0000 @@ -0,0 +1,108 @@ +> 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_expand_map() on behalf of Menu module. + */ +function menu_admin_menu_expand_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_expand_map() on behalf of Node module. + */ +function node_admin_menu_expand_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_expand_map() on behalf of Field UI module. + */ +function field_ui_admin_menu_expand_map() { + foreach (entity_get_info() as $obj_type => $info) { + foreach ($info['bundles'] as $bundle_name => $bundle_info) { + if (isset($bundle_info['admin'])) { + switch ($obj_type) { + case 'node': + // Provide replacement sets for content types, note that the + // fields (%field_ui_menu) are different for each content type. + $arguments = array(); + 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; + } + $path = $bundle_info['admin']['path']; + $map["$path/fields/%field_ui_menu"] = array( + 'parent' => "$path/fields", + 'arguments' => $arguments, + ); + } + } + } + return $map; +} + +/** + * Implements hook_admin_menu_expand_map() on behalf of Views UI module. + */ +function views_ui_admin_menu_expand_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.104 diff -u -p -r1.104 admin_menu.module --- admin_menu.module 4 Nov 2009 21:51:19 -0000 1.104 +++ admin_menu.module 12 Nov 2009 04:18:46 -0000 @@ -106,75 +106,11 @@ function admin_menu_menu() { * Implementation of hook_menu_alter(). */ function admin_menu_menu_alter(&$items) { - foreach ($items as $path => $item) { - if (!strncmp($path, 'admin/', 6) && isset($item['type'])) { - // To avoid namespace clashes, content-type menu items are registered - // on a separate path. Copy these items with the proper router path and - // make them visible. - // @todo Is there another possibility to re-assign these items to their - // true parent (admin/structure/types) during menu rebuild to avoid the - // (possibly) clashing copying? - if (strpos($path, 'admin/structure/node-type/') === 0) { - // Copy item with fixed router path. - $newpath = str_replace('/node-type/', '/types/', $path); - $items[$newpath] = $item; - // Use new item from here, but leave old intact to play nice with - // others. - $path = $newpath; - // Turn MENU_CALLBACK items into visible menu items. - if ($item['type'] == MENU_CALLBACK) { - $items[$path]['type'] = MENU_NORMAL_ITEM; - } - } - // Trick local tasks and actions into the visible menu tree. - if ($item['type'] & MENU_IS_LOCAL_TASK) { - $items[$path]['_visible'] = TRUE; - } - } - } - - // Remove local tasks on 'admin'. - $items['admin/by-task']['_visible'] = FALSE; - $items['admin/by-module']['_visible'] = FALSE; - // Move 'Add new content' to the start of the menu. - $items['node/add']['weight'] = -15; - // Flush client-side caches whenever the menu is rebuilt. admin_menu_flush_caches(); } /** - * Copy a menu router item to another location. - * - * Adjusts numeric path argument references for various menu router item - * properties. - * - * @param $item - * The existing menu router item. - * @param $old_path - * The existing path of $item, f.e. 'node/add'. - * @param $new_path - * The path to copy the menu router item to, f.e. 'admin/node/add'. - * @return - * The menu router item with adjusted path argument references. - */ -function admin_menu_copy_item($item, $old_path, $new_path) { - $offset = count(explode('/', $new_path)) - count(explode('/', $old_path)); - if ($offset != 0) { - foreach (array('page arguments', 'access arguments', 'load arguments', 'title arguments') as $args) { - if (!empty($item[$args])) { - foreach ($item[$args] as $key => $value) { - if (is_numeric($value)) { - $item[$args][$key] = $value + $offset; - } - } - } - } - } - return $item; -} - -/** * Implementation of hook_init(). * * We can't move this into admin_menu_footer(), because PHP-only based themes @@ -440,7 +376,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;