diff --git admin_menu.api.php admin_menu.api.php index 4ca7a65..44a5a1e 100644 --- admin_menu.api.php +++ admin_menu.api.php @@ -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 diff --git admin_menu.inc admin_menu.inc index 02320b1..72736bd 100644 --- admin_menu.inc +++ admin_menu.inc @@ -7,6 +7,327 @@ */ /** + * 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 to anonymous placeholders, since the menu + // system stores paths using anonymous placeholders. + // @todo Why specify named placeholders in the first place then? + $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); + + // Retrieve dynamic menu link tree for the expansion mappings. + $tree_dynamic = admin_menu_tree_dynamic($expand_map); + + // Merge local tasks with static menu tree. + $tree = menu_tree_all_data($menu_name); + admin_menu_merge_tree($tree, $tree_dynamic, array(), $hidden); + + return $tree; +} + +/** + * Load menu link trees for router paths containing dynamic arguments. + * + * @param $expand_map + * An array containing menu router path placeholder expansion argument + * mappings. + * + * @return + * An associative array whose keys are the parent paths of the menu router + * paths given in $expand_map as well as the parent paths of any child link + * deeper down the tree. The parent paths are used in admin_menu_merge_tree() + * to check whether anything needs to be merged. + * + * @see hook_admin_menu_map() + */ +function admin_menu_tree_dynamic(array $expand_map) { + $p_columns = array(); + for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) { + $p_columns[] = 'p' . $i; + } + + // Fetch p* columns for all router paths to expand. + $router_paths = array_keys($expand_map); + $plids = db_select('menu_links', 'ml') + ->fields('ml', $p_columns) + ->condition('router_path', $router_paths) + ->execute() + ->fetchAll(PDO::FETCH_ASSOC); + + // Unlikely, but possible. + if (empty($plids)) { + return array(); + } + + // Use queried plid columns to query sub-trees for the router paths. + $query = db_select('menu_links', 'ml'); + $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'))); + + // The retrieved menu link trees have to be ordered by depth, so parents + // always come before their children for the storage logic below. + // @todo Order by 'fit' or 'depth' instead for parent path handling below? + foreach ($p_columns as $column) { + $query->orderBy($column, 'ASC'); + } + + $db_or = db_or(); + foreach ($plids as $path_plids) { + $db_and = db_and(); + // Skip 0 (zero) columns. + foreach (array_filter($path_plids) as $column => $plid) { + $db_and->condition($column, $plid); + } + $db_or->condition($db_and); + } + $query->condition($db_or); + $result = $query + ->execute() + ->fetchAllAssoc('mlid', PDO::FETCH_ASSOC); + + // Store dynamic links grouped by parent path for later merging and assign + // placeholder expansion arguments. + $tree_dynamic = array(); + foreach ($result as $mlid => $link) { + // If contained in $expand_map, then this is a (first) parent, and we need + // to store by the defined 'parent' path for later merging, as well as + // provide the expansion map arguments to apply to the dynamic tree. + if (isset($expand_map[$link['path']])) { + $parent_path = $expand_map[$link['path']]['parent']; + $link['expand_map'] = $expand_map[$link['path']]['arguments']; + } + // Otherwise, just store this link keyed by its parent path; the expand_map + // is automatically derived from parent paths. + else { + $parent_path = $result[$link['plid']]['path']; + } + + $tree_dynamic[$parent_path][] = $link; + } + + return $tree_dynamic; +} + +/** + * Walk through the entire menu tree and merge in expanded dynamic menu links. + * + * @param &$tree + * A menu tree structure as returned by menu_tree_all_data(). + * @param $tree_dynamic + * A dynamic menu tree structure as returned by admin_menu_tree_dynamic(). + * @param $expand_map + * @todo Rename this argument, it's not the same $expand_map like elsewhere. + * Placeholder expansion arguments. + * @param $hidden + * An array containing links to hide, keyed by path. + * + * @see hook_admin_menu_map() + * @see menu_tree_all_data() + * @see admin_menu_tree_dynamic() + */ +function admin_menu_merge_tree(array &$tree, array $tree_dynamic, array $expand_map, array $hidden) { + foreach ($tree as $key => $data) { + $path = $data['link']['router_path']; + + // Recurse into regular menu tree. + if ($tree[$key]['below']) { + admin_menu_merge_tree($tree[$key]['below'], $tree_dynamic, $expand_map, $hidden); + } + // Hide link if requested, but only if there is no dynamic tree data for it. + elseif (isset($hidden[$path]) && !isset($tree_dynamic[$path])) { + $tree[$key]['link']['access'] = FALSE; + continue; + } + // Nothing to merge, if this parent path is not in our dynamic tree. + if (!isset($tree_dynamic[$path])) { + continue; + } + + foreach ($tree_dynamic[$path] as $link) { + // If the local task has custom placeholder expansion arguments set, + // merge them with parent arguments. + if (isset($link['expand_map'])) { + $expand_map = array_merge($expand_map, $link['expand_map']); + } + + // Set up path arguments map; depends on whether the item is dynamic + // (contains placeholders) or not. + if (strpos($link['path'], '%') === FALSE) { + echo "
"; var_dump("IN"); echo "
\n"; + // Build static item and subtree. + $map = explode('/', $link['path']); + $item = admin_menu_translate($link, $map); + admin_menu_merge_tree($item, $tree_dynamic, $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() + $active = ''; + $path = explode('/', $link['path']); + $load_functions = unserialize($link['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($link, $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, $tree_dynamic, 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; +} + +/** * Build the administration menu as renderable menu links. * * @param $tree @@ -22,8 +343,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. diff --git admin_menu.install admin_menu.install index 69c4d68..c0309ae 100644 --- admin_menu.install +++ admin_menu.install @@ -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(); +} + diff --git admin_menu.map.inc admin_menu.map.inc new file mode 100644 index 0000000..07f2051 --- /dev/null +++ admin_menu.map.inc @@ -0,0 +1,123 @@ + '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() { + $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() { + $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() { + $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() { + // @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; +} + diff --git admin_menu.module admin_menu.module index b093f50..1fee7f0 100644 --- admin_menu.module +++ admin_menu.module @@ -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;