diff --git includes/common.inc includes/common.inc index 3069aff..8689b4d 100644 --- includes/common.inc +++ includes/common.inc @@ -230,7 +230,7 @@ function drupal_get_profile() { function drupal_set_breadcrumb($breadcrumb = NULL) { $stored_breadcrumb = &drupal_static(__FUNCTION__); - if (!is_null($breadcrumb)) { + if (isset($breadcrumb)) { $stored_breadcrumb = $breadcrumb; } return $stored_breadcrumb; @@ -242,7 +242,7 @@ function drupal_set_breadcrumb($breadcrumb = NULL) { function drupal_get_breadcrumb() { $breadcrumb = drupal_set_breadcrumb(); - if (is_null($breadcrumb)) { + if (!isset($breadcrumb)) { $breadcrumb = menu_get_active_breadcrumb(); } @@ -2264,15 +2264,27 @@ function drupal_attributes(array $attributes = array()) { function l($text, $path, array $options = array()) { global $language_url; static $use_theme = NULL; + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['active'] = &drupal_static(__FUNCTION__); + } + $active_href = &$drupal_static_fast['active']; // Merge in defaults. $options += array( - 'attributes' => array(), - 'html' => FALSE, - ); + 'attributes' => array(), + 'html' => FALSE, + ); + // The active item needs to be cached by $_GET['q'] to account for an altered + // path via menu_set_active_item(). + if (!isset($active_href[$_GET['q']])) { + $item = menu_get_item(); + $active_href[$_GET['q']] = (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT ? $item['tab_root_href'] : $item['href']); + } // Append active class. - if (($path == $_GET['q'] || ($path == '' && drupal_is_front_page())) && + if (($path == $active_href[$_GET['q']] || ($path == '' && drupal_is_front_page())) && (empty($options['language']) || $options['language']->language == $language_url->language)) { $options['attributes']['class'][] = 'active'; } @@ -6177,7 +6189,7 @@ function drupal_write_record($table, &$record, $primary_keys = array()) { } if (!property_exists($object, $field)) { - // Skip fields that are not provided, default values are already known + // Skip fields that are not provided, default values are already known // by the database. continue; } diff --git includes/menu.inc includes/menu.inc index 0c69896..2a57666 100644 --- includes/menu.inc +++ includes/menu.inc @@ -141,9 +141,9 @@ define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB); * Menu type -- A hidden, internal callback, typically used for API calls. * * Callbacks simply register a path so that the correct function is fired - * when the URL is accessed. They are not shown in the menu. + * when the URL is accessed. They do not appear in menus or breadcrumbs. */ -define('MENU_CALLBACK', MENU_VISIBLE_IN_BREADCRUMB); +define('MENU_CALLBACK', 0x0000); /** * Menu type -- A normal menu item, hidden until enabled by an administrator. @@ -728,12 +728,26 @@ function _menu_translate(&$router_item, $map, $to_arg = FALSE) { // Generate the link path for the page request or local tasks. $link_map = explode('/', $router_item['path']); + if (isset($router_item['tab_root'])) { + $tab_root_map = explode('/', $router_item['tab_root']); + } + if (isset($router_item['tab_parent'])) { + $tab_parent_map = explode('/', $router_item['tab_parent']); + } for ($i = 0; $i < $router_item['number_parts']; $i++) { if ($link_map[$i] == '%') { $link_map[$i] = $path_map[$i]; } + if (isset($tab_root_map[$i]) && $tab_root_map[$i] == '%') { + $tab_root_map[$i] = $path_map[$i]; + } + if (isset($tab_parent_map[$i]) && $tab_parent_map[$i] == '%') { + $tab_parent_map[$i] = $path_map[$i]; + } } $router_item['href'] = implode('/', $link_map); + $router_item['tab_root_href'] = implode('/', $tab_root_map); + $router_item['tab_parent_href'] = implode('/', $tab_parent_map); $router_item['options'] = array(); _menu_check_access($router_item, $map); @@ -778,7 +792,11 @@ function menu_tail_to_arg($arg, $map, $index) { * preparation such as always calling to_arg functions * * @param $item - * A menu link + * A menu link. + * @param $translate + * (optional) Whether to try to translate a link containing dynamic path + * argument placeholders (%) based on the menu router item of the current + * path. Defaults to FALSE. Internally used for breadcrumbs only. * * @return * Returns the map of path arguments with objects loaded as defined in the @@ -789,8 +807,10 @@ function menu_tail_to_arg($arg, $map, $index) { * $item['options'] is unserialized; it is also changed within the call here * to $item['localized_options'] by _menu_item_localize(). */ -function _menu_link_translate(&$item) { - $item['options'] = unserialize($item['options']); +function _menu_link_translate(&$item, $translate = FALSE) { + if (!is_array($item['options'])) { + $item['options'] = unserialize($item['options']); + } if ($item['external']) { $item['access'] = 1; $map = array(); @@ -799,13 +819,40 @@ function _menu_link_translate(&$item) { $item['localized_options'] = $item['options']; } else { + // Complete the path of the menu link with elements from the current path, + // if it contains dynamic placeholders (%). $map = explode('/', $item['link_path']); - if (!empty($item['to_arg_functions'])) { - _menu_link_map_translate($map, $item['to_arg_functions']); + if (strpos($item['link_path'], '%') !== FALSE) { + // Invoke registered to_arg callbacks. + if (!empty($item['to_arg_functions'])) { + _menu_link_map_translate($map, $item['to_arg_functions']); + } + // Or try to derive the path argument map from the current router item, + // if this $item's path is within ) the router item's path. This means + // that if we are on the current path 'foo/%/bar/%/baz', then + // menu_get_item() will have translated the menu router item for the + // current path, and we can take over the argument map for a link like + // 'foo/%/bar'. This inheritance is only valid for breadcrumb links. + // @see _menu_tree_check_access() + // @see menu_get_active_breadcrumb() + elseif ($translate && ($current_router_item = menu_get_item())) { + // If $translate is TRUE, then this link is in the active trail. + // Only translate paths within the current path. + if (strpos($current_router_item['path'], $item['link_path']) === 0) { + $count = count($map); + $map = array_slice($current_router_item['original_map'], 0, $count); + $item['original_map'] = $map; + if (isset($current_router_item['map'])) { + $item['map'] = array_slice($current_router_item['map'], 0, $count); + } + // Reset access to check it (for the first time). + unset($item['access']); + } + } } $item['href'] = implode('/', $map); - // Note - skip callbacks without real values for their arguments. + // Skip links containing untranslated arguments. if (strpos($item['href'], '%') !== FALSE) { $item['access'] = FALSE; return FALSE; @@ -913,7 +960,7 @@ function menu_tree_output($tree) { // Pull out just the menu links we are going to render so that we // get an accurate count for the first/last classes. foreach ($tree as $data) { - if (!$data['link']['hidden']) { + if ($data['link']['access'] && !$data['link']['hidden']) { $items[] = $data; } } @@ -993,7 +1040,7 @@ function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) { // Use $mlid as a flag for whether the data being loaded is for the whole tree. $mlid = isset($link['mlid']) ? $link['mlid'] : 0; // Generate a cache ID (cid) specific for this $menu_name, $link, $language, and depth. - $cid = 'links:' . $menu_name . ':all-cid:' . $mlid . ':' . $GLOBALS['language']->language . ':' . (int) $max_depth; + $cid = 'links:' . $menu_name . ':all:' . $mlid . ':' . $GLOBALS['language']->language . ':' . (int) $max_depth; if (!isset($tree[$cid])) { // If the static variable doesn't have the data, check {cache_menu}. @@ -1042,9 +1089,13 @@ function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) { * field, see http://drupal.org/node/141866 for more. * * @param $menu_name - * The named menu links to return + * The named menu links to return. * @param $max_depth - * Optional maximum depth of links to retrieve. + * (optional) The maximum depth of links to retrieve. + * @param $only_active_trail + * (optional) Whether to only return the links in the active trail (TRUE) + * instead of all links on every level of the menu link tree (FALSE). Defaults + * to FALSE. Internally used for breadcrumbs only. * * @return * An array of menu links, in the order they should be rendered. The array @@ -1053,7 +1104,7 @@ function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) { * submenu below the link if there is one, and it is a subtree that has the * same structure described for the top-level array. */ -function menu_tree_page_data($menu_name, $max_depth = NULL) { +function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail = FALSE) { $tree = &drupal_static(__FUNCTION__, array()); // Load the menu item corresponding to the current page. @@ -1062,7 +1113,18 @@ function menu_tree_page_data($menu_name, $max_depth = NULL) { $max_depth = min($max_depth, MENU_MAX_DEPTH); } // Generate a cache ID (cid) specific for this page. - $cid = 'links:' . $menu_name . ':page-cid:' . $item['href'] . ':' . $GLOBALS['language']->language . ':' . (int) $item['access'] . ':' . (int) $max_depth; + $cid = 'links:' . $menu_name . ':page:' . $item['href'] . ':' . $GLOBALS['language']->language . ':' . (int) $item['access'] . ':' . (int) $max_depth; + // If we are asked for the active trail only, and $menu_name has not been + // built and cached for this page yet, then this likely means that it + // won't be built anymore, as this function is invoked from + // template_process_page(). So in order to not build a giant menu tree + // that needs to be checked for access on all levels, we simply check + // whether we have the menu already in cache, or otherwise, build a minimum + // tree containing the breadcrumb/active trail only. + // @see menu_set_active_trail() + if (!isset($tree[$cid]) && $only_active_trail) { + $cid .= ':trail'; + } if (!isset($tree[$cid])) { // If the static variable doesn't have the data, check {cache_menu}. @@ -1078,57 +1140,39 @@ function menu_tree_page_data($menu_name, $max_depth = NULL) { 'min_depth' => 1, 'max_depth' => $max_depth, ); + // Parent mlids; used both as key and value to ensure uniqueness. + // We always want all the top-level links with plid == 0. + $active_trail = array(0 => 0); + // If the item for the current page is accessible, build the tree // parameters accordingly. if ($item['access']) { - // Check whether a menu link exists that corresponds to the current path. - $args[] = $item['href']; - if (drupal_is_front_page()) { - $args[] = ''; - } - $active_link = db_select('menu_links') - ->fields('menu_links', array( - 'p1', - 'p2', - 'p3', - 'p4', - 'p5', - 'p6', - 'p7', - 'p8', - )) - ->condition('menu_name', $menu_name) - ->condition('link_path', $args, 'IN') - ->execute()->fetchAssoc(); - - if (empty($active_link)) { - // If no link exists, we may be on a local task that's not in the links. - // TODO: Handle the case like a local task on a specific node in the menu. - $active_link = db_select('menu_links') - ->fields('menu_links', array( - 'p1', - 'p2', - 'p3', - 'p4', - 'p5', - 'p6', - 'p7', - 'p8', - )) - ->condition('menu_name', $menu_name) - ->condition('link_path', $item['tab_root']) - ->execute()->fetchAssoc(); + // Find a menu link corresponding to the current path. + if ($active_link = menu_link_get_preferred()) { + // The active link may only be taken into account to build the + // active trail, if it resides in the requested menu. Otherwise, + // we'd needlessly re-run _menu_build_tree() queries for every menu + // on every page. + if ($active_link['menu_name'] == $menu_name) { + // Use all the coordinates, except the last one because there + // can be no child beyond the last column. + for ($i = 1; $i < MENU_MAX_DEPTH; $i++) { + if ($active_link['p' . $i]) { + $active_trail[$active_link['p' . $i]] = $active_link['p' . $i]; + } + } + // If we are asked to build links for the active trail only, skip + // the entire 'expanded' handling. + if ($only_active_trail) { + $tree_parameters['only_active_trail'] = TRUE; + } + } } - - // We always want all the top-level links with plid == 0. - $active_link[] = '0'; - - // Use array_values() so that the indices are numeric. - $parents = $active_link = array_unique(array_values($active_link)); + $parents = $active_trail; $expanded = variable_get('menu_expanded', array()); // Check whether the current menu has any links set to be expanded. - if (in_array($menu_name, $expanded)) { + if (!$only_active_trail && in_array($menu_name, $expanded)) { // Collect all the links set to be expanded, and then add all of // their children to the list as well. do { @@ -1142,19 +1186,19 @@ function menu_tree_page_data($menu_name, $max_depth = NULL) { ->execute(); $num_rows = FALSE; foreach ($result as $item) { - $parents[] = $item['mlid']; + $parents[$item['mlid']] = $item['mlid']; $num_rows = TRUE; } } while ($num_rows); } $tree_parameters['expanded'] = $parents; - $tree_parameters['active_trail'] = $active_link; + $tree_parameters['active_trail'] = $active_trail; } - // Otherwise, only show the top-level menu items when access is denied. + // If access is denied, we only show top-level links in menus. else { - $tree_parameters['expanded'] = array(0); + $tree_parameters['expanded'] = $active_trail; + $tree_parameters['active_trail'] = $active_trail; } - // Cache the tree building parameters using the page-specific cid. cache_set($cid, $tree_parameters, 'cache_menu'); } @@ -1178,9 +1222,12 @@ function menu_tree_page_data($menu_name, $max_depth = NULL) { * (optional) An associative array of build parameters. Possible keys: * - expanded: An array of parent link ids to return only menu links that are * children of one of the plids in this list. If empty, the whole menu tree - * is built. + * is built, unless 'only_active_trail' is TRUE. * - active_trail: An array of mlids, representing the coordinates of the * currently active menu link. + * - only_active_trail: Whether to only return links that are in the active + * trail. This option is ignored, if 'expanded' is non-empty. Internally + * used for breadcrumbs only. * - min_depth: The minimum depth of menu links in the resulting tree. * Defaults to 1, which is the default to build a whole tree for a menu, i.e. * excluding menu container itself. @@ -1241,6 +1288,8 @@ function _menu_build_tree($menu_name, array $parameters = array()) { 'page_callback', 'page_arguments', 'delivery_callback', + 'tab_parent', + 'tab_root', 'title', 'title_callback', 'title_arguments', @@ -1256,6 +1305,9 @@ function _menu_build_tree($menu_name, array $parameters = array()) { if (!empty($parameters['expanded'])) { $query->condition('ml.plid', $parameters['expanded'], 'IN'); } + elseif (!empty($parameters['only_active_trail'])) { + $query->condition('ml.mlid', $parameters['active_trail'], 'IN'); + } $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1); if ($min_depth != 1) { $query->condition('ml.depth', $min_depth, '>='); @@ -1269,8 +1321,8 @@ function _menu_build_tree($menu_name, array $parameters = array()) { foreach ($query->execute() as $item) { $links[] = $item; } - $active_link = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array()); - $data['tree'] = menu_tree_data($links, $active_link, $min_depth); + $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array()); + $data['tree'] = menu_tree_data($links, $active_trail, $min_depth); $data['node_links'] = array(); menu_tree_collect_node_links($data['tree'], $data['node_links']); @@ -1315,7 +1367,6 @@ function menu_tree_collect_node_links(&$tree, &$node_links) { * menu_tree_collect_node_links(). */ function menu_tree_check_access(&$tree, $node_links = array()) { - if ($node_links) { $nids = array_keys($node_links); $select = db_select('node', 'n'); @@ -1331,7 +1382,6 @@ function menu_tree_check_access(&$tree, $node_links = array()) { } } _menu_tree_check_access($tree); - return; } /** @@ -1342,7 +1392,7 @@ function _menu_tree_check_access(&$tree) { foreach ($tree as $key => $v) { $item = &$tree[$key]['link']; _menu_link_translate($item); - if ($item['access']) { + if ($item['access'] || ($item['in_active_trail'] && strpos($item['href'], '%') !== FALSE)) { if ($tree[$key]['below']) { _menu_tree_check_access($tree[$key]['below']); } @@ -1729,7 +1779,7 @@ function menu_local_tasks($level = 0) { // If this router item points to its parent, start from the parents to // compute tabs and actions. if ($router_item && ($router_item['type'] & MENU_LINKS_TO_PARENT)) { - $router_item = menu_get_item($router_item['tab_parent']); + $router_item = menu_get_item($router_item['tab_parent_href']); } // If we failed to fetch a router item or the current user doesn't have @@ -2103,7 +2153,7 @@ function menu_set_active_item($path) { } /** - * Sets or gets the active trail (path to root menu root) of the current page. + * Sets or gets the active trail (path to menu tree root) of the current page. * * @param $new_trail * Menu trail to set, or NULL to use previously-set or calculated trail. If @@ -2132,77 +2182,161 @@ function menu_set_active_trail($new_trail = NULL) { } elseif (!isset($trail)) { $trail = array(); - $trail[] = array('title' => t('Home'), 'href' => '', 'localized_options' => array(), 'type' => 0); - $item = menu_get_item(); - - // Check whether the current item is a local task (displayed as a tab). - if ($item['tab_parent']) { - // The title of a local task is used for the tab, never the page title. - // Thus, replace it with the item corresponding to the root path to get - // the relevant href and title. For example, the menu item corresponding - // to 'admin' is used when on the 'By module' tab at 'admin/by-module'. - $parts = explode('/', $item['tab_root']); - $args = arg(); - // Replace wildcards in the root path using the current path. - foreach ($parts as $index => $part) { - if ($part == '%') { - $parts[$index] = $args[$index]; - } - } - // Retrieve the menu item using the root path after wildcard replacement. - $root_item = menu_get_item(implode('/', $parts)); - if ($root_item && $root_item['access']) { - $item = $root_item; - } + $trail[] = array( + 'title' => t('Home'), + 'href' => '', + 'link_path' => '', + 'localized_options' => array(), + 'type' => 0, + ); + + // Try to retrieve a menu link corresponding to the current path. If more + // than one exists, the link from the most preferred menu is returned. + $preferred_link = menu_link_get_preferred(); + $current_item = menu_get_item(); + + // There is a link for the current path. + if ($preferred_link) { + // Pass TRUE for $only_active_trail to make menu_tree_page_data() build + // a stripped down menu tree containing the active trail only, in case + // the given menu has not been built in this request yet. + $tree = menu_tree_page_data($preferred_link['menu_name'], NULL, TRUE); + list($key, $curr) = each($tree); } - $menu_names = menu_get_active_menu_names(); - $curr = FALSE; - // Determine if the current page is a link in any of the active menus. - if ($menu_names) { - $query = db_select('menu_links', 'ml'); - $query->fields('ml', array('menu_name')); - $query->condition('ml.link_path', $item['href']); - $query->condition('ml.menu_name', $menu_names, 'IN'); - $result = $query->execute(); - $found = array(); - foreach ($result as $menu) { - $found[] = $menu->menu_name; - } - // The $menu_names array is ordered, so take the first one that matches. - $found_menu_names = array_intersect($menu_names, $found); - $name = current($found_menu_names); - if ($name !== FALSE) { - $tree = menu_tree_page_data($name); - list($key, $curr) = each($tree); - } + // There is no link for the current path. + else { + $preferred_link = $current_item; + $curr = FALSE; } while ($curr) { - // Terminate the loop when we find the current path in the active trail. - if ($curr['link']['href'] == $item['href']) { - $trail[] = $curr['link']; - $curr = FALSE; - } - else { - // Add the link if it's in the active trail, then move to the link below. - if ($curr['link']['in_active_trail']) { - $trail[] = $curr['link']; - $tree = $curr['below'] ? $curr['below'] : array(); + $link = $curr['link']; + if ($link['in_active_trail']) { + // Add the link to the trail, unless it links to its parent. + if (!($link['type'] & MENU_LINKS_TO_PARENT)) { + // The menu tree for the active trail may contain additional links + // that have not been translated yet, since they contain dynamic + // argument placeholders (%). Such links are not contained in regular + // menu trees, and have only been loaded for the additional + // translation that happens here, so as to be able to display them in + // the breadcumb for the current page. + // @see _menu_tree_check_access() + // @see _menu_link_translate() + if (strpos($link['href'], '%') !== FALSE) { + _menu_link_translate($link, TRUE); + } + if ($link['access']) { + $trail[] = $link; + } } - list($key, $curr) = each($tree); + $tree = $curr['below'] ? $curr['below'] : array(); } + list($key, $curr) = each($tree); } // Make sure the current page is in the trail (needed for the page title), - // but exclude tabs and the front page. - $last = count($trail) - 1; - if ($trail[$last]['href'] != $item['href'] && !(bool) ($item['type'] & MENU_IS_LOCAL_TASK) && !drupal_is_front_page()) { - $trail[] = $item; + // if the link's type allows it to be shown in the breadcrumb. Also exclude + // it if we are on the front page. + $last = end($trail); + if ($last['href'] != $preferred_link['href'] && ($preferred_link['type'] & MENU_VISIBLE_IN_BREADCRUMB) == MENU_VISIBLE_IN_BREADCRUMB && !drupal_is_front_page()) { + $trail[] = $preferred_link; } } return $trail; } /** + * Lookup the preferred menu link for a given system path. + * + * @param $path + * The path, for example 'node/5'. The function will find the corresponding + * menu link ('node/5' if it exists, or fallback to 'node/%'). + * + * @return + * A fully translated menu link, or NULL if not matching menu link was + * found. The most specific menu link ('node/5' preferred over 'node/%') in + * the most preferred menu (as defined by menu_get_active_menu_names()) is + * returned. + */ +function menu_link_get_preferred($path = NULL) { + $preferred_links = &drupal_static(__FUNCTION__); + + if (!isset($path)) { + $path = $_GET['q']; + } + + if (!isset($preferred_links[$path])) { + $preferred_links[$path] = FALSE; + + // Look for the correct menu link by building a list of candidate paths, + // which are ordered by priority (translated hrefs are preferred over + // untranslated paths). Afterwards, the most relevant path is picked from + // the menus, ordered by menu preference. + $item = menu_get_item($path); + $path_candidates = array(); + // 1. The current item href. + $path_candidates[$item['href']] = $item['href']; + // 2. The tab root href of the current item (if any). + if ($item['tab_parent'] && ($tab_root = menu_get_item($item['tab_root_href']))) { + $path_candidates[$tab_root['href']] = $tab_root['href']; + } + // 3. The current item path (with wildcards). + $path_candidates[$item['path']] = $item['path']; + // 4. The tab root path of the current item (if any). + if (!empty($tab_root)) { + $path_candidates[$tab_root['path']] = $tab_root['path']; + } + + // Retrieve a list of menu names, ordered by preference. + $menu_names = menu_get_active_menu_names(); + + $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC)); + $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path'); + $query->fields('ml'); + // Weight must be taken from {menu_links}, not {menu_router}. + $query->fields('m', array_diff(drupal_schema_fields_sql('menu_router'), array('weight'))); + $query->condition('ml.menu_name', $menu_names, 'IN'); + $query->condition('ml.link_path', $path_candidates, 'IN'); + // Include links + // - appearing in trees (MENU_VISIBLE_IN_TREE). + // - appearing in breadcrumbs (MENU_VISIBLE_IN_BREADCRUMB), since + // breadcrumbs are based on regular menu link trees. + // - not mapping to any router path (NULL). + $query->condition(db_or() + ->condition('m.type', MENU_VISIBLE_IN_TREE, '&') + ->condition('m.type', MENU_VISIBLE_IN_BREADCRUMB, '&') + ->isNull('m.type') + ); + + // Sort candidates by link path and menu name. + $candidates = array(); + foreach ($query->execute() as $candidate) { + $candidates[$candidate['link_path']][$candidate['menu_name']] = $candidate; + } + + // Pick the most specific link, in the most preferred menu. + foreach ($path_candidates as $link_path) { + if (!isset($candidates[$link_path])) { + continue; + } + foreach ($menu_names as $menu_name) { + if (!isset($candidates[$link_path][$menu_name])) { + continue; + } + $candidate_item = $candidates[$link_path][$menu_name]; + $map = explode('/', $path); + _menu_translate($candidate_item, $map); + if ($candidate_item['access']) { + $preferred_links[$path] = $candidate_item; + } + break 2; + } + } + } + + return $preferred_links[$path]; +} + +/** * Gets the active trail (path to root menu root) of the current page. * * See menu_set_active_trail() for details of return value. @@ -2213,6 +2347,8 @@ function menu_get_active_trail() { /** * Get the breadcrumb for the current page, as determined by the active trail. + * + * @see menu_set_active_trail() */ function menu_get_active_breadcrumb() { $breadcrumb = array(); @@ -2223,17 +2359,38 @@ function menu_get_active_breadcrumb() { } $item = menu_get_item(); - if ($item && $item['access']) { + if (!empty($item['access'])) { $active_trail = menu_get_active_trail(); - foreach ($active_trail as $parent) { - $breadcrumb[] = l($parent['title'], $parent['href'], $parent['localized_options']); - } - $end = end($active_trail); + // Allow modules to alter the breadcrumb, if possible, as that is much + // faster than rebuilding an entirely new active trail. + drupal_alter('menu_breadcrumb', $active_trail, $item); // Don't show a link to the current page in the breadcrumb trail. - if ($item['href'] == $end['href'] || (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT && $end['href'] != '')) { - array_pop($breadcrumb); + $end = end($active_trail); + if ($item['href'] == $end['href']) { + array_pop($active_trail); + } + + // Remove the tab root (parent) if the current path links to its parent. + // Normally, the tab root link is included in the breadcrumb, as soon as we + // are on a local task or any other child link. However, if we are on a + // default local task (e.g., node/%/view), then we do not want the tab root + // link (e.g., node/%) to appear, as it would be identical to the current + // page. Since this behavior also needs to work recursively (i.e., on + // default local tasks of default local tasks), and since the last non-task + // link in the trail is used as page title (see menu_get_active_title()), + // this condition cannot be cleanly integrated into menu_get_active_trail(). + // menu_get_active_trail() already skips all links that link to their parent + // (commonly MENU_DEFAULT_LOCAL_TASK). In order to also hide the parent link + // itself, we always remove the last link in the trail, if the current + // router item links to its parent. + if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) { + array_pop($active_trail); + } + + foreach ($active_trail as $parent) { + $breadcrumb[] = l($parent['title'], $parent['href'], $parent['localized_options']); } } return $breadcrumb; @@ -2319,6 +2476,8 @@ function menu_reset_static_cache() { drupal_static_reset('menu_tree_all_data'); drupal_static_reset('menu_tree_page_data'); drupal_static_reset('menu_load_all'); + drupal_static_reset('menu_link_get_preferred'); + drupal_static_reset('l'); } /** diff --git includes/theme.inc includes/theme.inc index 658128c..0175c2d 100644 --- includes/theme.inc +++ includes/theme.inc @@ -2257,7 +2257,6 @@ function template_preprocess_page(&$variables) { $variables['base_path'] = base_path(); $variables['front_page'] = url(); - $variables['breadcrumb'] = theme('breadcrumb', array('breadcrumb' => drupal_get_breadcrumb())); $variables['feed_icons'] = drupal_get_feeds(); $variables['language'] = $GLOBALS['language']; $variables['language']->dir = $GLOBALS['language']->direction ? 'rtl' : 'ltr'; @@ -2269,7 +2268,6 @@ function template_preprocess_page(&$variables) { $variables['site_name'] = (theme_get_setting('toggle_name') ? filter_xss_admin(variable_get('site_name', 'Drupal')) : ''); $variables['site_slogan'] = (theme_get_setting('toggle_slogan') ? filter_xss_admin(variable_get('site_slogan', '')) : ''); $variables['tabs'] = theme('menu_local_tasks'); - $variables['title'] = drupal_get_title(); if ($node = menu_get_object()) { $variables['node'] = $node; @@ -2282,6 +2280,29 @@ function template_preprocess_page(&$variables) { } /** + * Process variables for page.tpl.php + * + * Perform final addition and modification of variables before passing into + * the template. To customize these variables, simply set them in an earlier + * step already. + * + * @see template_preprocess_page() + * @see page.tpl.php + */ +function template_process_page(&$variables) { + if (!isset($variables['breadcrumb'])) { + // Build the breadcrumb last, so as to increase the chance of being able to + // re-use the cache of an already rendered menu containing the active link + // for the current page. + // @see menu_tree_page_data() + $variables['breadcrumb'] = theme('breadcrumb', array('breadcrumb' => drupal_get_breadcrumb())); + } + if (!isset($variables['title'])) { + $variables['title'] = drupal_get_title(); + } +} + +/** * Process variables for html.tpl.php * * Perform final addition and modification of variables before passing into diff --git modules/aggregator/aggregator.module modules/aggregator/aggregator.module index 0ebbc0a..e0c2e02 100644 --- modules/aggregator/aggregator.module +++ modules/aggregator/aggregator.module @@ -134,7 +134,6 @@ function aggregator_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('aggregator_admin_remove_feed', 5), 'access arguments' => array('administer news feeds'), - 'type' => MENU_CALLBACK, 'file' => 'aggregator.admin.inc', ); $items['admin/config/services/aggregator/update/%aggregator_feed'] = array( @@ -142,7 +141,6 @@ function aggregator_menu() { 'page callback' => 'aggregator_admin_refresh_feed', 'page arguments' => array(5), 'access arguments' => array('administer news feeds'), - 'type' => MENU_CALLBACK, 'file' => 'aggregator.admin.inc', ); $items['admin/config/services/aggregator/list'] = array( @@ -227,7 +225,6 @@ function aggregator_menu() { 'page callback' => 'aggregator_page_source', 'page arguments' => array(2), 'access arguments' => array('access news feeds'), - 'type' => MENU_CALLBACK, 'file' => 'aggregator.pages.inc', ); $items['aggregator/sources/%aggregator_feed/view'] = array( @@ -257,7 +254,6 @@ function aggregator_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('aggregator_form_feed', 6), 'access arguments' => array('administer news feeds'), - 'type' => MENU_CALLBACK, 'file' => 'aggregator.admin.inc', ); $items['admin/config/services/aggregator/edit/category/%aggregator_category'] = array( @@ -265,7 +261,6 @@ function aggregator_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('aggregator_form_category', 6), 'access arguments' => array('administer news feeds'), - 'type' => MENU_CALLBACK, 'file' => 'aggregator.admin.inc', ); diff --git modules/book/book.module modules/book/book.module index 1a53dcc..4362ff0 100644 --- modules/book/book.module +++ modules/book/book.module @@ -182,7 +182,6 @@ function book_menu() { 'access callback' => '_book_outline_remove_access', 'access arguments' => array(1), 'theme callback' => '_node_custom_theme', - 'type' => MENU_CALLBACK, 'file' => 'book.pages.inc', ); diff --git modules/comment/comment.module modules/comment/comment.module index 8b8eef6..4ae6eaa 100644 --- modules/comment/comment.module +++ modules/comment/comment.module @@ -241,7 +241,6 @@ function comment_menu() { 'page callback' => 'comment_permalink', 'page arguments' => array(1), 'access arguments' => array('access comments'), - 'type' => MENU_CALLBACK, ); $items['comment/%/view'] = array( 'title' => 'View comment', @@ -264,7 +263,6 @@ function comment_menu() { 'page callback' => 'comment_approve', 'page arguments' => array(1), 'access arguments' => array('administer comments'), - 'type' => MENU_CALLBACK, 'file' => 'comment.pages.inc', 'weight' => 1, ); @@ -283,7 +281,6 @@ function comment_menu() { 'page arguments' => array(2), 'access callback' => 'node_access', 'access arguments' => array('view', 2), - 'type' => MENU_CALLBACK, 'file' => 'comment.pages.inc', ); diff --git modules/contact/contact.module modules/contact/contact.module index 5beceaf..6687cfe 100644 --- modules/contact/contact.module +++ modules/contact/contact.module @@ -77,7 +77,6 @@ function contact_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('contact_category_edit_form', 4), 'access arguments' => array('administer contact forms'), - 'type' => MENU_CALLBACK, 'file' => 'contact.admin.inc', ); $items['admin/structure/contact/delete/%contact'] = array( @@ -85,7 +84,6 @@ function contact_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('contact_category_delete_form', 4), 'access arguments' => array('administer contact forms'), - 'type' => MENU_CALLBACK, 'file' => 'contact.admin.inc', ); $items['contact'] = array( diff --git modules/dashboard/dashboard.module modules/dashboard/dashboard.module index fad6a9e..1882184 100644 --- modules/dashboard/dashboard.module +++ modules/dashboard/dashboard.module @@ -53,7 +53,6 @@ function dashboard_menu() { 'page callback' => 'dashboard_admin', 'page arguments' => array(TRUE), 'access arguments' => array('access dashboard'), - 'type' => MENU_CALLBACK, ); $items['admin/dashboard/drawer'] = array( 'page callback' => 'dashboard_show_disabled', diff --git modules/dblog/dblog.module modules/dblog/dblog.module index 252c401..ebda962 100644 --- modules/dblog/dblog.module +++ modules/dblog/dblog.module @@ -67,7 +67,6 @@ function dblog_menu() { 'page callback' => 'dblog_event', 'page arguments' => array(3), 'access arguments' => array('access site reports'), - 'type' => MENU_CALLBACK, 'file' => 'dblog.admin.inc', ); diff --git modules/field/modules/options/options.test modules/field/modules/options/options.test index 3dfd9d9..afb5e11 100644 --- modules/field/modules/options/options.test +++ modules/field/modules/options/options.test @@ -77,7 +77,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { field_test_entity_save($entity); // With no field data, no buttons are checked. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertNoFieldChecked("edit-card-1-$langcode-0"); $this->assertNoFieldChecked("edit-card-1-$langcode-1"); $this->assertNoFieldChecked("edit-card-1-$langcode-2"); @@ -89,7 +89,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { $this->assertFieldValues($entity_init, 'card_1', $langcode, array(0)); // Check that the selected button is checked. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertFieldChecked("edit-card-1-$langcode-0"); $this->assertNoFieldChecked("edit-card-1-$langcode-1"); $this->assertNoFieldChecked("edit-card-1-$langcode-2"); @@ -104,7 +104,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { field_update_field($this->card_1); $instance['required'] = TRUE; field_update_instance($instance); - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertFieldChecked("edit-card-1-$langcode-99"); } @@ -134,7 +134,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { field_test_entity_save($entity); // Display form: with no field data, nothing is checked. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertNoFieldChecked("edit-card-2-$langcode-0"); $this->assertNoFieldChecked("edit-card-2-$langcode-1"); $this->assertNoFieldChecked("edit-card-2-$langcode-2"); @@ -150,7 +150,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { $this->assertFieldValues($entity_init, 'card_2', $langcode, array(0, 2)); // Display form: check that the right options are selected. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertFieldChecked("edit-card-2-$langcode-0"); $this->assertNoFieldChecked("edit-card-2-$langcode-1"); $this->assertFieldChecked("edit-card-2-$langcode-2"); @@ -165,7 +165,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { $this->assertFieldValues($entity_init, 'card_2', $langcode, array(0)); // Display form: check that the right options are selected. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertFieldChecked("edit-card-2-$langcode-0"); $this->assertNoFieldChecked("edit-card-2-$langcode-1"); $this->assertNoFieldChecked("edit-card-2-$langcode-2"); @@ -194,7 +194,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { field_update_field($this->card_2); $instance['required'] = TRUE; field_update_instance($instance); - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertFieldChecked("edit-card-2-$langcode-99"); } @@ -222,7 +222,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { field_test_entity_save($entity); // Display form. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); // A required field without any value has a "none" option. $this->assertTrue($this->xpath('//select[@id=:id]//option[@value="_none" and text()=:label]', array(':id' => 'edit-card-1-' . $langcode, ':label' => t('- Select a value -'))), t('A required select list has a "Select a value" choice.')); @@ -244,7 +244,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { $this->assertFieldValues($entity_init, 'card_1', $langcode, array(0)); // Display form: check that the right options are selected. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); // A required field with a value has no 'none' option. $this->assertFalse($this->xpath('//select[@id=:id]//option[@value="_none"]', array(':id' => 'edit-card-1-' . $langcode)), t('A required select list with an actual value has no "none" choice.')); $this->assertOptionSelected("edit-card-1-$langcode", 0); @@ -256,12 +256,12 @@ class OptionsWidgetsTestCase extends FieldTestCase { field_update_instance($instance); // Display form. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); // A non-required field has a 'none' option. $this->assertTrue($this->xpath('//select[@id=:id]//option[@value="_none" and text()=:label]', array(':id' => 'edit-card-1-' . $langcode, ':label' => t('- None -'))), t('A non-required select list has a "None" choice.')); // Submit form: Unselect the option. $edit = array("card_1[$langcode]" => '_none'); - $this->drupalPost('test-entity/' . $entity->ftid .'/edit', $edit, t('Save')); + $this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save')); $this->assertFieldValues($entity_init, 'card_1', $langcode, array()); // Test optgroups. @@ -271,7 +271,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { field_update_field($this->card_1); // Display form: with no field data, nothing is selected - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertNoOptionSelected("edit-card-1-$langcode", 0); $this->assertNoOptionSelected("edit-card-1-$langcode", 1); $this->assertNoOptionSelected("edit-card-1-$langcode", 2); @@ -284,14 +284,14 @@ class OptionsWidgetsTestCase extends FieldTestCase { $this->assertFieldValues($entity_init, 'card_1', $langcode, array(0)); // Display form: check that the right options are selected. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertOptionSelected("edit-card-1-$langcode", 0); $this->assertNoOptionSelected("edit-card-1-$langcode", 1); $this->assertNoOptionSelected("edit-card-1-$langcode", 2); // Submit form: Unselect the option. $edit = array("card_1[$langcode]" => '_none'); - $this->drupalPost('test-entity/' . $entity->ftid .'/edit', $edit, t('Save')); + $this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save')); $this->assertFieldValues($entity_init, 'card_1', $langcode, array()); } @@ -318,7 +318,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { field_test_entity_save($entity); // Display form: with no field data, nothing is selected. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertNoOptionSelected("edit-card-2-$langcode", 0); $this->assertNoOptionSelected("edit-card-2-$langcode", 1); $this->assertNoOptionSelected("edit-card-2-$langcode", 2); @@ -330,7 +330,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { $this->assertFieldValues($entity_init, 'card_2', $langcode, array(0, 2)); // Display form: check that the right options are selected. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertOptionSelected("edit-card-2-$langcode", 0); $this->assertNoOptionSelected("edit-card-2-$langcode", 1); $this->assertOptionSelected("edit-card-2-$langcode", 2); @@ -341,7 +341,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { $this->assertFieldValues($entity_init, 'card_2', $langcode, array(0)); // Display form: check that the right options are selected. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertOptionSelected("edit-card-2-$langcode", 0); $this->assertNoOptionSelected("edit-card-2-$langcode", 1); $this->assertNoOptionSelected("edit-card-2-$langcode", 2); @@ -361,18 +361,18 @@ class OptionsWidgetsTestCase extends FieldTestCase { // Check that the 'none' option has no efect if actual options are selected // as well. $edit = array("card_2[$langcode][]" => array('_none' => '_none', 0 => 0)); - $this->drupalPost('test-entity/' . $entity->ftid .'/edit', $edit, t('Save')); + $this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save')); $this->assertFieldValues($entity_init, 'card_2', $langcode, array(0)); // Check that selecting the 'none' option empties the field. $edit = array("card_2[$langcode][]" => array('_none' => '_none')); - $this->drupalPost('test-entity/' . $entity->ftid .'/edit', $edit, t('Save')); + $this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save')); $this->assertFieldValues($entity_init, 'card_2', $langcode, array()); // A required select list does not have an empty key. $instance['required'] = TRUE; field_update_instance($instance); - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertFalse($this->xpath('//select[@id=:id]//option[@value=""]', array(':id' => 'edit-card-2-' . $langcode)), t('A required select list does not have an empty key.')); // We do not have to test that a required select list with one option is @@ -388,7 +388,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { field_update_instance($instance); // Display form: with no field data, nothing is selected. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertNoOptionSelected("edit-card-2-$langcode", 0); $this->assertNoOptionSelected("edit-card-2-$langcode", 1); $this->assertNoOptionSelected("edit-card-2-$langcode", 2); @@ -401,14 +401,14 @@ class OptionsWidgetsTestCase extends FieldTestCase { $this->assertFieldValues($entity_init, 'card_2', $langcode, array(0)); // Display form: check that the right options are selected. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertOptionSelected("edit-card-2-$langcode", 0); $this->assertNoOptionSelected("edit-card-2-$langcode", 1); $this->assertNoOptionSelected("edit-card-2-$langcode", 2); // Submit form: Unselect the option. $edit = array("card_2[$langcode][]" => array('_none' => '_none')); - $this->drupalPost('test-entity/' . $entity->ftid .'/edit', $edit, t('Save')); + $this->drupalPost('test-entity/manage/' . $entity->ftid . '/edit', $edit, t('Save')); $this->assertFieldValues($entity_init, 'card_2', $langcode, array()); } @@ -435,7 +435,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { field_test_entity_save($entity); // Display form: with no field data, option is unchecked. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertNoFieldChecked("edit-bool-$langcode"); $this->assertRaw('Some dangerous & unescaped markup', t('Option text was properly filtered.')); @@ -445,7 +445,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { $this->assertFieldValues($entity_init, 'bool', $langcode, array(0)); // Display form: check that the right options are selected. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertFieldChecked("edit-bool-$langcode"); // Submit form: uncheck the option. @@ -454,7 +454,7 @@ class OptionsWidgetsTestCase extends FieldTestCase { $this->assertFieldValues($entity_init, 'bool', $langcode, array(1)); // Display form: with 'off' value, option is unchecked. - $this->drupalGet('test-entity/' . $entity->ftid .'/edit'); + $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); $this->assertNoFieldChecked("edit-bool-$langcode"); } } diff --git modules/field/modules/text/text.test modules/field/modules/text/text.test index 1843f36..9d45ed6 100644 --- modules/field/modules/text/text.test +++ modules/field/modules/text/text.test @@ -115,7 +115,7 @@ class TextFieldTestCase extends DrupalWebTestCase { "{$this->field_name}[$langcode][0][value]" => $value, ); $this->drupalPost(NULL, $edit, t('Save')); - preg_match('|test-entity/(\d+)/edit|', $this->url, $match); + preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match); $id = $match[1]; $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), t('Entity was created')); @@ -184,7 +184,7 @@ class TextFieldTestCase extends DrupalWebTestCase { "{$this->field_name}[$langcode][0][value]" => $value, ); $this->drupalPost(NULL, $edit, t('Save')); - preg_match('|test-entity/(\d+)/edit|', $this->url, $match); + preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match); $id = $match[1]; $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), t('Entity was created')); @@ -210,7 +210,7 @@ class TextFieldTestCase extends DrupalWebTestCase { // Display edition form. // We should now have a 'text format' selector. - $this->drupalGet('test-entity/' . $id . '/edit'); + $this->drupalGet('test-entity/manage/' . $id . '/edit'); $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', t('Widget is displayed')); $this->assertFieldByName("{$this->field_name}[$langcode][0][format]", '', t('Format selector is displayed')); diff --git modules/field/tests/field.test modules/field/tests/field.test index 41fecdb..25b65af 100644 --- modules/field/tests/field.test +++ modules/field/tests/field.test @@ -1259,14 +1259,14 @@ class FieldFormTestCase extends FieldTestCase { $value = mt_rand(1, 127); $edit = array("{$this->field_name}[$langcode][0][value]" => $value); $this->drupalPost(NULL, $edit, t('Save')); - preg_match('|test-entity/(\d+)/edit|', $this->url, $match); + preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match); $id = $match[1]; $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created'); $entity = field_test_entity_test_load($id); $this->assertEqual($entity->{$this->field_name}[$langcode][0]['value'], $value, 'Field value was saved'); // Display edit form. - $this->drupalGet('test-entity/' . $id . '/edit'); + $this->drupalGet('test-entity/manage/' . $id . '/edit'); $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", $value, 'Widget is displayed with the correct default value'); $this->assertNoField("{$this->field_name}[$langcode][1][value]", 'No extraneous widget is displayed'); @@ -1281,7 +1281,7 @@ class FieldFormTestCase extends FieldTestCase { // Empty the field. $value = ''; $edit = array("{$this->field_name}[$langcode][0][value]" => $value); - $this->drupalPost('test-entity/' . $id . '/edit', $edit, t('Save')); + $this->drupalPost('test-entity/manage/' . $id . '/edit', $edit, t('Save')); $this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), 'Entity was updated'); $entity = field_test_entity_test_load($id); $this->assertIdentical($entity->{$this->field_name}, array(), 'Field was emptied'); @@ -1306,7 +1306,7 @@ class FieldFormTestCase extends FieldTestCase { $value = mt_rand(1, 127); $edit = array("{$this->field_name}[$langcode][0][value]" => $value); $this->drupalPost(NULL, $edit, t('Save')); - preg_match('|test-entity/(\d+)/edit|', $this->url, $match); + preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match); $id = $match[1]; $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created'); $entity = field_test_entity_test_load($id); @@ -1315,7 +1315,7 @@ class FieldFormTestCase extends FieldTestCase { // Edit with missing required value. $value = ''; $edit = array("{$this->field_name}[$langcode][0][value]" => $value); - $this->drupalPost('test-entity/' . $id . '/edit', $edit, t('Save')); + $this->drupalPost('test-entity/manage/' . $id . '/edit', $edit, t('Save')); $this->assertRaw(t('!name field is required.', array('!name' => $this->instance['label'])), 'Required field with no value fails validation'); } @@ -1384,7 +1384,7 @@ class FieldFormTestCase extends FieldTestCase { // Submit the form and create the entity. $this->drupalPost(NULL, $edit, t('Save')); - preg_match('|test-entity/(\d+)/edit|', $this->url, $match); + preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match); $id = $match[1]; $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created'); $entity = field_test_entity_test_load($id); @@ -1474,7 +1474,7 @@ class FieldFormTestCase extends FieldTestCase { // Create entity with three values. $edit = array("{$this->field_name}[$langcode]" => '1, 2, 3'); $this->drupalPost(NULL, $edit, t('Save')); - preg_match('|test-entity/(\d+)/edit|', $this->url, $match); + preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match); $id = $match[1]; // Check that the values were saved. @@ -1482,7 +1482,7 @@ class FieldFormTestCase extends FieldTestCase { $this->assertFieldValues($entity_init, $this->field_name, $langcode, array(1, 2, 3)); // Display the form, check that the values are correctly filled in. - $this->drupalGet('test-entity/' . $id . '/edit'); + $this->drupalGet('test-entity/manage/' . $id . '/edit'); $this->assertFieldByName("{$this->field_name}[$langcode]", '1, 2, 3', t('Widget is displayed.')); // Submit the form with more values than the field accepts. @@ -1529,7 +1529,7 @@ class FieldFormTestCase extends FieldTestCase { // Create entity. $edit = array("{$field_name}[$langcode][0][value]" => 1); $this->drupalPost(NULL, $edit, t('Save')); - preg_match('|test-entity/(\d+)/edit|', $this->url, $match); + preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match); $id = $match[1]; // Check that the default value was saved. @@ -1539,7 +1539,7 @@ class FieldFormTestCase extends FieldTestCase { // Create a new revision. $edit = array("{$field_name}[$langcode][0][value]" => 2, 'revision' => TRUE); - $this->drupalPost('test-entity/' . $id . '/edit', $edit, t('Save')); + $this->drupalPost('test-entity/manage/' . $id . '/edit', $edit, t('Save')); // Check that the new revision has the expected values. $entity = field_test_entity_test_load($id); @@ -2721,7 +2721,7 @@ class FieldTranslationsTestCase extends FieldTestCase { // Create a new revision. $langcode = field_valid_language(NULL); $edit = array("{$field_name}[$langcode][0][value]" => $entity->{$field_name}[$langcode][0]['value'], 'revision' => TRUE); - $this->drupalPost('test-entity/' . $eid . '/edit', $edit, t('Save')); + $this->drupalPost('test-entity/manage/' . $eid . '/edit', $edit, t('Save')); // Check translation revisions. $this->checkTranslationRevisions($eid, $eid, $available_languages); diff --git modules/field/tests/field_test.entity.inc modules/field/tests/field_test.entity.inc index 0ed6b4f..54f63af 100644 --- modules/field/tests/field_test.entity.inc +++ modules/field/tests/field_test.entity.inc @@ -392,7 +392,7 @@ function field_test_entity_form_submit($form, &$form_state) { drupal_set_message($message); if ($entity->ftid) { - $form_state['redirect'] = 'test-entity/' . $entity->ftid . '/edit'; + $form_state['redirect'] = 'test-entity/manage/' . $entity->ftid . '/edit'; } else { // Error on save. diff --git modules/field/tests/field_test.module modules/field/tests/field_test.module index e031a48..e71a431 100644 --- modules/field/tests/field_test.module +++ modules/field/tests/field_test.module @@ -52,10 +52,10 @@ function field_test_menu() { 'type' => MENU_NORMAL_ITEM, ); } - $items['test-entity/%field_test_entity_test/edit'] = array( + $items['test-entity/manage/%field_test_entity_test/edit'] = array( 'title' => 'Edit test entity', 'page callback' => 'field_test_entity_edit', - 'page arguments' => array(1), + 'page arguments' => array(2), 'access arguments' => array('administer field_test content'), 'type' => MENU_NORMAL_ITEM, ); diff --git modules/field_ui/field_ui.module modules/field_ui/field_ui.module index 48690a5..833bb26 100644 --- modules/field_ui/field_ui.module +++ modules/field_ui/field_ui.module @@ -114,7 +114,6 @@ function field_ui_menu() { 'title arguments' => array($field_position), 'page callback' => 'drupal_get_form', 'page arguments' => array('field_ui_field_edit_form', $field_position), - 'type' => MENU_LOCAL_TASK, 'file' => 'field_ui.admin.inc', ) + $access; $items["$path/fields/%field_ui_menu/edit"] = array( diff --git modules/filter/filter.module modules/filter/filter.module index d36253d..fe1960a 100644 --- modules/filter/filter.module +++ modules/filter/filter.module @@ -114,7 +114,6 @@ function filter_menu() { 'file' => 'filter.admin.inc', ); $items['admin/config/content/formats/%filter_format'] = array( - 'type' => MENU_CALLBACK, 'title callback' => 'filter_admin_format_title', 'title arguments' => array(4), 'page callback' => 'filter_admin_format_page', @@ -128,7 +127,6 @@ function filter_menu() { 'page arguments' => array('filter_admin_delete', 4), 'access callback' => '_filter_delete_format_access', 'access arguments' => array(4), - 'type' => MENU_CALLBACK, 'file' => 'filter.admin.inc', ); return $items; diff --git modules/forum/forum.module modules/forum/forum.module index 4f5ca81..6cb2732 100644 --- modules/forum/forum.module +++ modules/forum/forum.module @@ -147,7 +147,6 @@ function forum_menu() { 'page callback' => 'forum_form_main', 'page arguments' => array('container', 5), 'access arguments' => array('administer forums'), - 'type' => MENU_CALLBACK, 'file' => 'forum.admin.inc', ); $items['admin/structure/forum/edit/forum/%taxonomy_term'] = array( @@ -155,7 +154,6 @@ function forum_menu() { 'page callback' => 'forum_form_main', 'page arguments' => array('forum', 5), 'access arguments' => array('administer forums'), - 'type' => MENU_CALLBACK, 'file' => 'forum.admin.inc', ); return $items; diff --git modules/forum/forum.test modules/forum/forum.test index 267dec7..6ad9c3d 100644 --- modules/forum/forum.test +++ modules/forum/forum.test @@ -25,9 +25,25 @@ class ForumTestCase extends DrupalWebTestCase { function setUp() { parent::setUp('taxonomy', 'comment', 'forum'); // Create users. - $this->admin_user = $this->drupalCreateUser(array('administer blocks', 'administer forums', 'administer menu', 'administer taxonomy', 'create forum content')); // 'access administration pages')); - $this->edit_any_topics_user = $this->drupalCreateUser(array('create forum content', 'edit any forum content', 'delete any forum content', 'access administration pages')); - $this->edit_own_topics_user = $this->drupalCreateUser(array('create forum content', 'edit own forum content', 'delete own forum content')); + $this->admin_user = $this->drupalCreateUser(array( + 'access administration pages', + 'administer blocks', + 'administer forums', + 'administer menu', + 'administer taxonomy', + 'create forum content', + )); + $this->edit_any_topics_user = $this->drupalCreateUser(array( + 'access administration pages', + 'create forum content', + 'edit any forum content', + 'delete any forum content', + )); + $this->edit_own_topics_user = $this->drupalCreateUser(array( + 'create forum content', + 'edit own forum content', + 'delete own forum content', + )); $this->web_user = $this->drupalCreateUser(array()); } diff --git modules/help/help.module modules/help/help.module index 90dd729..bdfe9ab 100644 --- modules/help/help.module +++ modules/help/help.module @@ -25,7 +25,6 @@ function help_menu() { 'page callback' => 'help_page', 'page arguments' => array(2), 'access arguments' => array('access administration pages'), - 'type' => MENU_CALLBACK, 'file' => 'help.admin.inc', ); } diff --git modules/image/image.module modules/image/image.module index 71c96e3..c874b81 100644 --- modules/image/image.module +++ modules/image/image.module @@ -125,7 +125,6 @@ function image_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('image_style_form', 5), 'access arguments' => array('administer image styles'), - 'type' => MENU_CALLBACK, 'file' => 'image.admin.inc', ); $items['admin/config/media/image-styles/delete/%image_style'] = array( @@ -135,7 +134,6 @@ function image_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('image_style_delete_form', 5), 'access arguments' => array('administer image styles'), - 'type' => MENU_CALLBACK, 'file' => 'image.admin.inc', ); $items['admin/config/media/image-styles/revert/%image_style'] = array( @@ -145,7 +143,6 @@ function image_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('image_style_revert_form', 5), 'access arguments' => array('administer image styles'), - 'type' => MENU_CALLBACK, 'file' => 'image.admin.inc', ); $items['admin/config/media/image-styles/edit/%image_style/effects/%image_effect'] = array( @@ -155,7 +152,6 @@ function image_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('image_effect_form', 5, 7), 'access arguments' => array('administer image styles'), - 'type' => MENU_CALLBACK, 'file' => 'image.admin.inc', ); $items['admin/config/media/image-styles/edit/%image_style/effects/%image_effect/delete'] = array( @@ -165,7 +161,6 @@ function image_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('image_effect_delete_form', 5, 7), 'access arguments' => array('administer image styles'), - 'type' => MENU_CALLBACK, 'file' => 'image.admin.inc', ); $items['admin/config/media/image-styles/edit/%image_style/add/%image_effect_definition'] = array( @@ -175,7 +170,6 @@ function image_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('image_effect_form', 5, 7), 'access arguments' => array('administer image styles'), - 'type' => MENU_CALLBACK, 'file' => 'image.admin.inc', ); diff --git modules/locale/locale.module modules/locale/locale.module index 162faa0..1a3120a 100644 --- modules/locale/locale.module +++ modules/locale/locale.module @@ -150,7 +150,6 @@ function locale_menu() { 'page arguments' => array('locale_languages_edit_form', 5), 'access arguments' => array('administer languages'), 'file' => 'locale.admin.inc', - 'type' => MENU_CALLBACK, ); $items['admin/config/regional/language/delete/%'] = array( 'title' => 'Confirm', @@ -158,7 +157,6 @@ function locale_menu() { 'page arguments' => array('locale_languages_delete_form', 5), 'access arguments' => array('administer languages'), 'file' => 'locale.admin.inc', - 'type' => MENU_CALLBACK, ); // Translation functionality @@ -205,7 +203,6 @@ function locale_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('locale_translate_edit_form', 5), 'access arguments' => array('translate interface'), - 'type' => MENU_CALLBACK, 'file' => 'locale.admin.inc', ); $items['admin/config/regional/translate/delete/%'] = array( @@ -213,7 +210,6 @@ function locale_menu() { 'page callback' => 'locale_translate_delete_page', 'page arguments' => array(5), 'access arguments' => array('translate interface'), - 'type' => MENU_CALLBACK, 'file' => 'locale.admin.inc', ); @@ -233,7 +229,6 @@ function locale_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('locale_date_format_form', 5), 'access arguments' => array('administer site configuration'), - 'type' => MENU_CALLBACK, 'file' => 'locale.admin.inc', ); $items['admin/config/regional/date-time/locale/%/reset'] = array( @@ -242,7 +237,6 @@ function locale_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('locale_date_format_reset_form', 5), 'access arguments' => array('administer site configuration'), - 'type' => MENU_CALLBACK, 'file' => 'locale.admin.inc', ); diff --git modules/menu/menu.module modules/menu/menu.module index 4fed193..2fe910f 100644 --- modules/menu/menu.module +++ modules/menu/menu.module @@ -95,7 +95,6 @@ function menu_menu() { 'title callback' => 'menu_overview_title', 'title arguments' => array(4), 'access arguments' => array('administer menu'), - 'type' => MENU_CALLBACK, 'file' => 'menu.admin.inc', ); $items['admin/structure/menu/manage/%menu/list'] = array( @@ -126,7 +125,6 @@ function menu_menu() { 'page callback' => 'menu_delete_menu_page', 'page arguments' => array(4), 'access arguments' => array('administer menu'), - 'type' => MENU_CALLBACK, 'file' => 'menu.admin.inc', ); $items['admin/structure/menu/item/%menu_link/edit'] = array( @@ -134,7 +132,6 @@ function menu_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('menu_edit_item', 'edit', 4, NULL), 'access arguments' => array('administer menu'), - 'type' => MENU_CALLBACK, 'file' => 'menu.admin.inc', ); $items['admin/structure/menu/item/%menu_link/reset'] = array( @@ -142,7 +139,6 @@ function menu_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('menu_reset_item_confirm', 4), 'access arguments' => array('administer menu'), - 'type' => MENU_CALLBACK, 'file' => 'menu.admin.inc', ); $items['admin/structure/menu/item/%menu_link/delete'] = array( @@ -150,7 +146,6 @@ function menu_menu() { 'page callback' => 'menu_item_delete_page', 'page arguments' => array(4), 'access arguments' => array('administer menu'), - 'type' => MENU_CALLBACK, 'file' => 'menu.admin.inc', ); return $items; @@ -175,29 +170,10 @@ function menu_theme() { /** * Implements hook_enable(). * - * Add a link for each custom menu. + * @todo Fatal error when running tests without this. */ function menu_enable() { menu_rebuild(); - $base_link = db_query("SELECT mlid AS plid, menu_name FROM {menu_links} WHERE link_path = 'admin/structure/menu' AND module = 'system'")->fetchAssoc(); - $base_link['router_path'] = 'admin/structure/menu/manage/%'; - $base_link['module'] = 'menu'; - $result = db_query("SELECT * FROM {menu_custom}", array(), array('fetch' => PDO::FETCH_ASSOC)); - foreach ($result as $menu) { - // $link is passed by reference to menu_link_save(), so we make a copy of $base_link. - $link = $base_link; - $link['mlid'] = 0; - $link['link_title'] = $menu['title']; - $link['link_path'] = 'admin/structure/menu/manage/' . $menu['menu_name']; - $menu_link = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path AND plid = :plid", array( - ':path' => $link['link_path'], - ':plid' => $link['plid'] - )) - ->fetchField(); - if (!$menu_link) { - menu_link_save($link); - } - } menu_cache_clear_all(); } @@ -707,7 +683,7 @@ function menu_form_node_type_form_alter(&$form, $form_state) { $form['menu']['menu_options'] = array( '#type' => 'checkboxes', '#title' => t('Available menus'), - '#default_value' => variable_get('menu_options_' . $type->type, array('main-menu' => 'main-menu')), + '#default_value' => variable_get('menu_options_' . $type->type, array('main-menu')), '#options' => $menu_options, '#description' => t('The menus available to place links in for this content type.'), ); diff --git modules/menu/menu.test modules/menu/menu.test index e2cfe7e..18c5d50 100644 --- modules/menu/menu.test +++ modules/menu/menu.test @@ -112,14 +112,14 @@ class MenuTestCase extends DrupalWebTestCase { // Assert the new menu. $this->drupalGet('admin/structure/menu/manage/' . $menu_name . '/edit'); - $this->assertText($title, t('Custom menu was added.')); + $this->assertRaw($title, t('Custom menu was added.')); // Edit the menu. $new_title = $this->randomName(16); $menu['title'] = $new_title; menu_save($menu); $this->drupalGet('admin/structure/menu/manage/' . $menu_name . '/edit'); - $this->assertText($new_title, t('Custom menu was edited.')); + $this->assertRaw($new_title, t('Custom menu was edited.')); } /** diff --git modules/node/content_types.inc modules/node/content_types.inc index ce74427..5360485 100644 --- modules/node/content_types.inc +++ modules/node/content_types.inc @@ -326,6 +326,7 @@ function node_type_form_submit($form, &$form_state) { return; } + form_state_values_clean($form_state); $variables = $form_state['values']; // Remove everything that's been saved already - whatever's left is assumed @@ -336,8 +337,6 @@ function node_type_form_submit($form, &$form_state) { } } - unset($variables['form_token'], $variables['op'], $variables['submit'], $variables['delete'], $variables['reset'], $variables['form_id'], $variables['form_build_id']); - // Save or reset persistent variable values. foreach ($variables as $key => $value) { $variable_new = $key . '_' . $type->type; diff --git modules/node/node.module modules/node/node.module index ffd6ed8..219314a 100644 --- modules/node/node.module +++ modules/node/node.module @@ -1848,12 +1848,10 @@ function node_menu() { 'title' => 'Delete', 'page arguments' => array('node_type_delete_confirm', 4), 'access arguments' => array('administer content types'), - 'type' => MENU_CALLBACK, 'file' => 'content_types.inc', ); $items['node'] = array( - 'title' => 'Content', 'page callback' => 'node_page_default', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, @@ -1899,7 +1897,6 @@ function node_menu() { 'page arguments' => array(1), 'access callback' => 'node_access', 'access arguments' => array('view', 1), - 'type' => MENU_CALLBACK, ); $items['node/%node/view'] = array( 'title' => 'View', @@ -1947,7 +1944,6 @@ function node_menu() { 'page arguments' => array(1, TRUE), 'access callback' => '_node_revision_access', 'access arguments' => array(1), - 'type' => MENU_CALLBACK, ); $items['node/%node/revisions/%/revert'] = array( 'title' => 'Revert to earlier revision', @@ -1956,7 +1952,6 @@ function node_menu() { 'page arguments' => array('node_revision_revert_confirm', 1), 'access callback' => '_node_revision_access', 'access arguments' => array(1, 'update'), - 'type' => MENU_CALLBACK, 'file' => 'node.pages.inc', ); $items['node/%node/revisions/%/delete'] = array( @@ -1966,7 +1961,6 @@ function node_menu() { 'page arguments' => array('node_revision_delete_confirm', 1), 'access callback' => '_node_revision_access', 'access arguments' => array(1, 'delete'), - 'type' => MENU_CALLBACK, 'file' => 'node.pages.inc', ); return $items; diff --git modules/openid/openid.module modules/openid/openid.module index c5fb70c..5ea7b7e 100644 --- modules/openid/openid.module +++ modules/openid/openid.module @@ -32,7 +32,6 @@ function openid_menu() { 'page arguments' => array('openid_user_delete_form', 1), 'access callback' => 'user_edit_access', 'access arguments' => array(1), - 'type' => MENU_CALLBACK, 'file' => 'openid.pages.inc', ); return $items; diff --git modules/path/path.module modules/path/path.module index bc4f480..654b93a 100644 --- modules/path/path.module +++ modules/path/path.module @@ -66,7 +66,6 @@ function path_menu() { 'page callback' => 'path_admin_edit', 'page arguments' => array(5), 'access arguments' => array('administer url aliases'), - 'type' => MENU_CALLBACK, 'file' => 'path.admin.inc', ); $items['admin/config/search/path/delete/%path'] = array( @@ -74,7 +73,6 @@ function path_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('path_admin_delete_confirm', 5), 'access arguments' => array('administer url aliases'), - 'type' => MENU_CALLBACK, 'file' => 'path.admin.inc', ); $items['admin/config/search/path/list'] = array( diff --git modules/profile/profile.module modules/profile/profile.module index efeb768..e20b0f3 100644 --- modules/profile/profile.module +++ modules/profile/profile.module @@ -99,7 +99,6 @@ function profile_menu() { 'page arguments' => array('profile_field_form'), 'access arguments' => array('administer users'), 'file' => 'profile.admin.inc', - 'type' => MENU_CALLBACK, ); $items['admin/config/people/profile/autocomplete'] = array( 'title' => 'Profile category autocomplete', @@ -114,7 +113,6 @@ function profile_menu() { 'page arguments' => array('profile_field_form'), 'access arguments' => array('administer users'), 'file' => 'profile.admin.inc', - 'type' => MENU_CALLBACK, ); $items['admin/config/people/profile/delete'] = array( 'title' => 'Delete field', @@ -122,7 +120,6 @@ function profile_menu() { 'page arguments' => array('profile_field_delete'), 'access arguments' => array('administer users'), 'file' => 'profile.admin.inc', - 'type' => MENU_CALLBACK, ); $items['profile/autocomplete'] = array( 'title' => 'Profile autocomplete', diff --git modules/search/search.module modules/search/search.module index 2772c8e..69555fb 100644 --- modules/search/search.module +++ modules/search/search.module @@ -182,7 +182,6 @@ function search_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('search_reindex_confirm'), 'access arguments' => array('administer search'), - 'type' => MENU_CALLBACK, 'file' => 'search.admin.inc', ); diff --git modules/search/search.test modules/search/search.test index 3dbbd1b..a3827cb 100644 --- modules/search/search.test +++ modules/search/search.test @@ -236,13 +236,16 @@ class SearchMatchTestCase extends DrupalWebTestCase { } } -class SearchBikeShed extends DrupalWebTestCase { +/** + * Tests the bike shed text on no results page, and text on the search page. + */ +class SearchPageText extends DrupalWebTestCase { protected $searching_user; public static function getInfo() { return array( - 'name' => 'Bike shed', - 'description' => 'Tests the bike shed text on the no results page.', + 'name' => 'Search page text', + 'description' => 'Tests the bike shed text on the no results page, and various other text on search pages.', 'group' => 'Search' ); } @@ -251,18 +254,31 @@ class SearchBikeShed extends DrupalWebTestCase { parent::setUp('search'); // Create user. - $this->searching_user = $this->drupalCreateUser(array('search content')); + $this->searching_user = $this->drupalCreateUser(array('search content', 'access user profiles')); } - function testFailedSearch() { + /** + * Tests the failed search text, and various other text on the search page. + */ + function testSearchText() { $this->drupalLogin($this->searching_user); $this->drupalGet('search/node'); $this->assertText(t('Enter your keywords')); + $this->assertText(t('Search')); + $title = t('Search') . ' | Drupal'; + $this->assertTitle($title, 'Search page title is correct'); $edit = array(); $edit['keys'] = 'bike shed ' . $this->randomName(); $this->drupalPost('search/node', $edit, t('Search')); $this->assertText(t('Consider loosening your query with OR. bike OR shed will often show more results than bike shed.'), t('Help text is displayed when search returns no results.')); + $this->assertText(t('Search')); + $this->assertTitle($title, 'Search page title is correct'); + + $edit['keys'] = $this->searching_user->name; + $this->drupalPost('search/user', $edit, t('Search')); + $this->assertText(t('Search')); + $this->assertTitle($title, 'Search page title is correct'); } } diff --git modules/shortcut/shortcut.module modules/shortcut/shortcut.module index 455961f..cb75587 100644 --- modules/shortcut/shortcut.module +++ modules/shortcut/shortcut.module @@ -91,7 +91,6 @@ function shortcut_menu() { 'title arguments' => array(4), 'access callback' => 'shortcut_set_edit_access', 'access arguments' => array(4), - 'type' => MENU_CALLBACK, 'file' => 'shortcut.admin.inc', ); $items['admin/config/user-interface/shortcut/%shortcut_set/links'] = array( @@ -114,7 +113,6 @@ function shortcut_menu() { 'page arguments' => array('shortcut_set_delete_form', 4), 'access callback' => 'shortcut_set_delete_access', 'access arguments' => array(4), - 'type' => MENU_CALLBACK, 'file' => 'shortcut.admin.inc', ); $items['admin/config/user-interface/shortcut/%shortcut_set/add-link'] = array( @@ -141,7 +139,6 @@ function shortcut_menu() { 'page arguments' => array('shortcut_link_edit', 5), 'access callback' => 'shortcut_link_access', 'access arguments' => array(5), - 'type' => MENU_CALLBACK, 'file' => 'shortcut.admin.inc', ); $items['admin/config/user-interface/shortcut/link/%menu_link/delete'] = array( @@ -150,7 +147,6 @@ function shortcut_menu() { 'page arguments' => array('shortcut_link_delete', 5), 'access callback' => 'shortcut_link_access', 'access arguments' => array(5), - 'type' => MENU_CALLBACK, 'file' => 'shortcut.admin.inc', ); $items['user/%user/shortcuts'] = array( diff --git modules/simpletest/drupal_web_test_case.php modules/simpletest/drupal_web_test_case.php index 088b39c..53ecfe0 100644 --- modules/simpletest/drupal_web_test_case.php +++ modules/simpletest/drupal_web_test_case.php @@ -2662,7 +2662,14 @@ class DrupalWebTestCase extends DrupalTestCase { * TRUE on pass, FALSE on fail. */ protected function assertTitle($title, $message = '', $group = 'Other') { - return $this->assertEqual(current($this->xpath('//title')), $title, $message, $group); + $actual = (string) current($this->xpath('//title')); + if (!$message) { + $message = t('Page title @actual is equal to @expected.', array( + '@actual' => var_export($actual, TRUE), + '@expected' => var_export($title, TRUE), + )); + } + return $this->assertEqual($actual, $title, $message, $group); } /** @@ -2678,7 +2685,14 @@ class DrupalWebTestCase extends DrupalTestCase { * TRUE on pass, FALSE on fail. */ protected function assertNoTitle($title, $message = '', $group = 'Other') { - return $this->assertNotEqual(current($this->xpath('//title')), $title, $message, $group); + $actual = (string) current($this->xpath('//title')); + if (!$message) { + $message = t('Page title @actual is not equal to @unexpected.', array( + '@actual' => var_export($actual, TRUE), + '@unexpected' => var_export($title, TRUE), + )); + } + return $this->assertNotEqual($actual, $title, $message, $group); } /** @@ -3016,6 +3030,24 @@ class DrupalWebTestCase extends DrupalTestCase { } /** + * Assert the page do not respond with the specified response code. + * + * @param $code + * Response code. For example 200 is a successful page request. For a list + * of all codes see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html. + * @param $message + * Message to display. + * + * @return + * Assertion result. + */ + protected function assertNoResponse($code, $message = '') { + $curl_code = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE); + $match = is_array($code) ? in_array($curl_code, $code) : $curl_code == $code; + return $this->assertFalse($match, $message ? $message : t('HTTP response not expected !code, actual !curl_code', array('!code' => $code, '!curl_code' => $curl_code)), t('Browser')); + } + + /** * Asserts that the most recently sent e-mail message has the given value. * * The field in $name must have the content described in $value. diff --git modules/simpletest/simpletest.module modules/simpletest/simpletest.module index 10fc6fb..e79cb88 100644 --- modules/simpletest/simpletest.module +++ modules/simpletest/simpletest.module @@ -56,7 +56,6 @@ function simpletest_menu() { 'page arguments' => array('simpletest_result_form', 5), 'description' => 'View result of tests.', 'access arguments' => array('administer unit tests'), - 'type' => MENU_CALLBACK, 'file' => 'simpletest.pages.inc', ); return $items; diff --git modules/simpletest/tests/menu.test modules/simpletest/tests/menu.test index 91058d5..8f24b3a 100644 --- modules/simpletest/tests/menu.test +++ modules/simpletest/tests/menu.test @@ -525,3 +525,555 @@ class MenuTreeDataTestCase extends DrupalUnitTestCase { return $this->assert($link1['mlid'] == $link2['mlid'], $message ? $message : t('First link is identical to second link')); } } + +/** + * Menu breadcrumbs related tests. + */ +class MenuBreadcrumbTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Breadcrumbs', + 'description' => 'Tests breadcrumbs functionality.', + 'group' => 'Menu', + ); + } + + function setUp() { + parent::setUp(array('menu_test')); + $perms = array_keys(module_invoke_all('permission')); + $this->admin_user = $this->drupalCreateUser($perms); + $this->drupalLogin($this->admin_user); + } + + /** + * Tests breadcrumbs on node and administrative paths. + */ + function testBreadCrumbs() { + // Prepare common base breadcrumb elements. + $home = array('' => 'Home'); + $admin = $home + array('admin' => t('Administer')); + $config = $admin + array('admin/config' => t('Configuration')); + $type = 'article'; + $langcode = LANGUAGE_NONE; + + // Verify breadcrumbs for default local tasks. + $expected = array( + 'menu-test' => t('Menu test root'), + ); + $title = t('Breadcrumbs test: Local tasks'); + $trail = $home + $expected; + $tree = $expected + array( + 'menu-test/breadcrumb/tasks' => $title, + ); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks', $trail, $title, $tree); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks/first', $trail, $title, $tree, FALSE); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks/first/first', $trail, $title, $tree, FALSE); + $trail += array( + 'menu-test/breadcrumb/tasks' => t('Breadcrumbs test: Local tasks'), + ); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks/first/second', $trail, $title, $tree, FALSE); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks/second', $trail, $title, $tree, FALSE); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks/second/first', $trail, $title, $tree, FALSE); + $trail += array( + 'menu-test/breadcrumb/tasks/second' => t('Second'), + ); + $this->assertBreadcrumb('menu-test/breadcrumb/tasks/second/second', $trail, $title, $tree, FALSE); + + // Verify Taxonomy administration breadcrumbs. + $trail = $admin + array( + 'admin/structure' => t('Structure'), + ); + $this->assertBreadcrumb('admin/structure/taxonomy', $trail); + + $trail += array( + 'admin/structure/taxonomy' => t('Taxonomy'), + ); + $this->assertBreadcrumb('admin/structure/taxonomy/tags', $trail); + $trail += array( + 'admin/structure/taxonomy/tags' => t('Tags'), + ); + $this->assertBreadcrumb('admin/structure/taxonomy/tags/edit', $trail); + $this->assertBreadcrumb('admin/structure/taxonomy/tags/fields', $trail); + $this->assertBreadcrumb('admin/structure/taxonomy/tags/add', $trail); + + // Verify Menu administration breadcrumbs. + $trail = $admin + array( + 'admin/structure' => t('Structure'), + ); + $this->assertBreadcrumb('admin/structure/menu', $trail); + + $trail += array( + 'admin/structure/menu' => t('Menus'), + ); + $this->assertBreadcrumb('admin/structure/menu/manage/navigation', $trail); + $trail += array( + 'admin/structure/menu/manage/navigation' => t('Navigation'), + ); + $this->assertBreadcrumb('admin/structure/menu/manage/navigation/edit', $trail); + $this->assertBreadcrumb('admin/structure/menu/manage/navigation/add', $trail); + + // Verify Node administration breadcrumbs. + $trail = $admin + array( + 'admin/structure' => t('Structure'), + 'admin/structure/types' => t('Content types'), + ); + $this->assertBreadcrumb('admin/structure/types/add', $trail); + $this->assertBreadcrumb("admin/structure/types/manage/$type", $trail); + $trail += array( + "admin/structure/types/manage/$type" => t('Article'), + ); + $this->assertBreadcrumb("admin/structure/types/manage/$type/fields", $trail); + $this->assertBreadcrumb("admin/structure/types/manage/$type/display", $trail); + $trail_teaser = $trail + array( + "admin/structure/types/manage/$type/display" => t('Manage display'), + ); + $this->assertBreadcrumb("admin/structure/types/manage/$type/display/teaser", $trail_teaser); + $this->assertBreadcrumb("admin/structure/types/manage/$type/comment/fields", $trail); + $this->assertBreadcrumb("admin/structure/types/manage/$type/comment/display", $trail); + $this->assertBreadcrumb("admin/structure/types/manage/$type/delete", $trail); + $trail += array( + "admin/structure/types/manage/$type/fields" => t('Manage fields'), + ); + $this->assertBreadcrumb("admin/structure/types/manage/$type/fields/body", $trail); + $trail += array( + "admin/structure/types/manage/$type/fields/body" => t('Body'), + ); + $this->assertBreadcrumb("admin/structure/types/manage/$type/fields/body/widget-type", $trail); + + // Verify Filter text format administration breadcrumbs. + $format = db_query_range("SELECT format, name FROM {filter_format}", 1, 1)->fetch(); + $format_id = $format->format; + $trail = $config + array( + 'admin/config/content' => t('Content authoring'), + ); + $this->assertBreadcrumb('admin/config/content/formats', $trail); + + $trail += array( + 'admin/config/content/formats' => t('Text formats'), + ); + $this->assertBreadcrumb('admin/config/content/formats/add', $trail); + $this->assertBreadcrumb("admin/config/content/formats/$format_id", $trail); + $trail += array( + "admin/config/content/formats/$format_id" => $format->name, + ); + $this->assertBreadcrumb("admin/config/content/formats/$format_id/delete", $trail); + + // Verify node breadcrumbs (without menu link). + $node1 = $this->drupalCreateNode(); + $nid1 = $node1->nid; + $trail = $home; + $this->assertBreadcrumb("node/$nid1", $trail); + // Also verify that the node does not appear elsewhere (e.g., menu trees). + $this->assertNoLink($node1->title); + // The node itself should not be contained in the breadcrumb on the default + // local task, since there is no difference between both pages. + $this->assertBreadcrumb("node/$nid1/view", $trail); + // Also verify that the node does not appear elsewhere (e.g., menu trees). + $this->assertNoLink($node1->title); + + $trail += array( + "node/$nid1" => $node1->title, + ); + $this->assertBreadcrumb("node/$nid1/edit", $trail); + + // Verify that breadcrumb on node listing page contains "Home" only. + $trail = array(); + $this->assertBreadcrumb('node', $trail); + + // Verify node breadcrumbs (in menu). + // Do this separately for Main menu and Navigation menu, since only the + // latter is a preferred menu by default. + // @todo Also test all themes? Manually testing led to the suspicion that + // breadcrumbs may differ, possibly due to template.php overrides. + $menus = array('main-menu', 'navigation'); + // Alter node type menu settings. + variable_set("menu_options_$type", $menus); + variable_set("menu_parent_$type", 'navigation:0'); + + foreach ($menus as $menu) { + // Create a parent node in the current menu. + $title = $this->randomName(); + $node2 = $this->drupalCreateNode(array( + 'type' => $type, + 'title' => $title, + 'menu' => array( + 'enabled' => 1, + 'link_title' => 'Parent ' . $title, + 'description' => '', + 'menu_name' => $menu, + 'plid' => 0, + ), + )); + $nid2 = $node2->nid; + + $trail = $home; + $tree = array( + "node/$nid2" => $node2->menu['link_title'], + ); + $this->assertBreadcrumb("node/$nid2", $trail, $node2->title, $tree); + // The node itself should not be contained in the breadcrumb on the + // default local task, since there is no difference between both pages. + $this->assertBreadcrumb("node/$nid2/view", $trail, $node2->title, $tree); + $trail += array( + "node/$nid2" => $node2->menu['link_title'], + ); + $this->assertBreadcrumb("node/$nid2/edit", $trail); + + // Create a child node in the current menu. + $title = $this->randomName(); + $node3 = $this->drupalCreateNode(array( + 'type' => $type, + 'title' => $title, + 'menu' => array( + 'enabled' => 1, + 'link_title' => 'Child ' . $title, + 'description' => '', + 'menu_name' => $menu, + 'plid' => $node2->menu['mlid'], + ), + )); + $nid3 = $node3->nid; + + $this->assertBreadcrumb("node/$nid3", $trail, $node3->title, $tree, FALSE); + // The node itself should not be contained in the breadcrumb on the + // default local task, since there is no difference between both pages. + $this->assertBreadcrumb("node/$nid3/view", $trail, $node3->title, $tree, FALSE); + $trail += array( + "node/$nid3" => $node3->menu['link_title'], + ); + $tree += array( + "node/$nid3" => $node3->menu['link_title'], + ); + $this->assertBreadcrumb("node/$nid3/edit", $trail); + + // Verify that node listing page still contains "Home" only. + $trail = array(); + $this->assertBreadcrumb('node', $trail); + + if ($menu == 'navigation') { + $parent = $node2; + $child = $node3; + } + } + + // Create a Navigation menu link for 'node', move the last parent node menu + // link below it, and verify a full breadcrumb for the last child node. + $menu = 'navigation'; + $edit = array( + 'link_title' => 'Root', + 'link_path' => 'node', + ); + $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save')); + $link = db_query('SELECT * FROM {menu_links} WHERE link_title = :title', array(':title' => 'Root'))->fetchAssoc(); + + $edit = array( + 'menu[parent]' => $link['menu_name'] . ':' . $link['mlid'], + ); + $this->drupalPost("node/{$parent->nid}/edit", $edit, t('Save')); + $expected = array( + "node" => $link['link_title'], + ); + $trail = $home + $expected; + $tree = $expected + array( + "node/{$parent->nid}" => $parent->menu['link_title'], + ); + $this->assertBreadcrumb(NULL, $trail, $parent->title, $tree); + $trail += array( + "node/{$parent->nid}" => $parent->menu['link_title'], + ); + $tree += array( + "node/{$child->nid}" => $child->menu['link_title'], + ); + $this->assertBreadcrumb("node/{$child->nid}", $trail, $child->title, $tree); + + // Add a taxonomy term/tag to last node, and add a link for that term to the + // Navigation menu. + $tags = array( + 'Drupal' => array(), + 'Breadcrumbs' => array(), + ); + $edit = array( + "field_tags[$langcode]" => implode(',', array_keys($tags)), + ); + $this->drupalPost("node/{$parent->nid}/edit", $edit, t('Save')); + + // Put both terms into a hierarchy Drupal » Breadcrumbs. Required for both + // the menu links and the terms itself, since taxonomy_term_page() resets + // the breadcrumb based on taxonomy term hierarchy. + $parent_tid = 0; + foreach ($tags as $name => $null) { + $terms = taxonomy_term_load_multiple(NULL, array('name' => $name)); + $term = reset($terms); + $tags[$name]['term'] = $term; + if ($parent_tid) { + $edit = array( + 'parent[]' => array($parent_tid), + ); + $this->drupalPost("taxonomy/term/{$term->tid}/edit", $edit, t('Save')); + } + $parent_tid = $term->tid; + } + $parent_mlid = 0; + foreach ($tags as $name => $data) { + $term = $data['term']; + $edit = array( + 'link_title' => "$name link", + 'link_path' => "taxonomy/term/{$term->tid}", + 'parent' => "$menu:{$parent_mlid}", + ); + $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save')); + $tags[$name]['link'] = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array( + ':title' => $edit['link_title'], + ':href' => $edit['link_path'], + ))->fetchAssoc(); + $tags[$name]['link']['link_path'] = $edit['link_path']; + $parent_mlid = $tags[$name]['link']['mlid']; + } + + // Verify expected breadcrumbs for menu links. + $trail = $home; + $tree = array(); + foreach ($tags as $name => $data) { + $term = $data['term']; + $link = $data['link']; + + $tree += array( + $link['link_path'] => $link['link_title'], + ); + // @todo Normally, you'd expect $term->name as page title here. + $this->assertBreadcrumb($link['link_path'], $trail, $link['link_title'], $tree); + $this->assertRaw(check_plain($parent->title), 'Tagged node found.'); + + // Additionally make sure that this link appears only once; i.e., the + // untranslated menu links automatically generated from menu router items + // ('taxonomy/term/%') should never be translated and appear in any menu + // other than the breadcrumb trail. + $elements = $this->xpath('//div[@id=:menu]/descendant::a[@href=:href]', array( + ':menu' => 'block-system-navigation', + ':href' => url($link['link_path']), + )); + $this->assertTrue(count($elements) == 1, "Link to {$link['link_path']} appears only once."); + + // Next iteration should expect this tag as parent link. + // Note: Term name, not link name, due to taxonomy_term_page(). + $trail += array( + $link['link_path'] => $term->name, + ); + } + + // Verify breadcrumbs on user and user/%. + // We need to log back in and out below, and cannot simply grant the + // 'administer users' permission, since user_page() makes your head explode. + user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array( + 'access user profiles', + )); + $this->drupalLogout(); + + // Verify breadcrumb on front page. + $this->assertBreadcrumb('', array()); + + $trail = $home; + $this->assertBreadcrumb('user', $trail, t('User account')); + $this->assertBreadcrumb('user/' . $this->admin_user->uid, $trail, $this->admin_user->name); + + $this->drupalLogin($this->admin_user); + $trail += array( + 'user/' . $this->admin_user->uid => $this->admin_user->name, + ); + $this->assertBreadcrumb('user/' . $this->admin_user->uid . '/edit', $trail, $this->admin_user->name); + + // Add a Navigation menu links for 'user' and $this->admin_user. + // Although it may be faster to manage these links via low-level API + // functions, there's a lot that can go wrong in doing so. + $edit = array( + 'link_title' => 'User', + 'link_path' => 'user', + ); + $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save')); + $link_user = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array( + ':title' => $edit['link_title'], + ':href' => $edit['link_path'], + ))->fetchAssoc(); + + $edit = array( + 'link_title' => $this->admin_user->name . ' link', + 'link_path' => 'user/' . $this->admin_user->uid, + ); + $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save')); + $link_admin_user = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array( + ':title' => $edit['link_title'], + ':href' => $edit['link_path'], + ))->fetchAssoc(); + + // Verify expected breadcrumbs for the two separate links. + $this->drupalLogout(); + $trail = $home; + $tree = array( + $link_user['link_path'] => $link_user['link_title'], + ); + $this->assertBreadcrumb('user', $trail, $link_user['link_title'], $tree); + $tree = array( + $link_admin_user['link_path'] => $link_admin_user['link_title'], + ); + $this->assertBreadcrumb('user/' . $this->admin_user->uid, $trail, $link_admin_user['link_title'], $tree); + + $this->drupalLogin($this->admin_user); + $trail += array( + $link_admin_user['link_path'] => $link_admin_user['link_title'], + ); + $this->assertBreadcrumb('user/' . $this->admin_user->uid . '/edit', $trail, $link_admin_user['link_title'], $tree, FALSE); + + // Move 'user/%' below 'user' and verify again. + $edit = array( + 'parent' => "$menu:{$link_user['mlid']}", + ); + $this->drupalPost("admin/structure/menu/item/{$link_admin_user['mlid']}/edit", $edit, t('Save')); + + $this->drupalLogout(); + $trail = $home; + $tree = array( + $link_user['link_path'] => $link_user['link_title'], + ); + $this->assertBreadcrumb('user', $trail, $link_user['link_title'], $tree); + $trail += array( + $link_user['link_path'] => $link_user['link_title'], + ); + $tree += array( + $link_admin_user['link_path'] => $link_admin_user['link_title'], + ); + $this->assertBreadcrumb('user/' . $this->admin_user->uid, $trail, $link_admin_user['link_title'], $tree); + + $this->drupalLogin($this->admin_user); + $trail += array( + $link_admin_user['link_path'] => $link_admin_user['link_title'], + ); + $this->assertBreadcrumb('user/' . $this->admin_user->uid . '/edit', $trail, $link_admin_user['link_title'], $tree, FALSE); + + // Create an only slightly privileged user being able to access site reports + // but not administration pages. + $this->web_user = $this->drupalCreateUser(array( + 'access site reports', + )); + $this->drupalLogin($this->web_user); + + // Verify that we can access recent log entries, there is a corresponding + // page title, and that the breadcrumb is empty (because the user is not + // able to access "Administer", so the trail cannot recurse into it). + $trail = array(); + $this->assertBreadcrumb('admin', $trail, t('Access denied')); + $this->assertResponse(403); + + $trail = $home; + $this->assertBreadcrumb('admin/reports', $trail, t('Reports')); + $this->assertNoResponse(403); + + $this->assertBreadcrumb('admin/reports/dblog', $trail, t('Recent log messages')); + $this->assertNoResponse(403); + } + + /** + * Assert that a given path shows certain breadcrumb links. + * + * @param string $goto + * (optional) A system path to pass to DrupalWebTestCase::drupalGet(). + * @param array $trail + * An associative array whose keys are expected breadcrumb link paths and + * whose values are expected breadcrumb link texts (not sanitized). + * @param string $page_title + * (optional) A page title to additionally assert via + * DrupalWebTestCase::assertTitle(). Without site name suffix. + * @param array $tree + * (optional) An associative array whose keys are link paths and whose + * values are link titles (not sanitized) of an expected active trail in a + * menu tree output on the page. + * @param $last_active + * (optional) Whether the last link in $tree is expected to be active (TRUE) + * or just to be in the active trail (FALSE). + */ + protected function assertBreadcrumb($goto, array $trail, $page_title = NULL, array $tree = array(), $last_active = TRUE) { + if (isset($goto)) { + $this->drupalGet($goto); + } + // Compare paths with actual breadcrumb. + $parts = $this->getParts(); + $pass = TRUE; + foreach ($trail as $path => $title) { + $url = url($path); + $part = array_shift($parts); + $pass = ($pass && $part['href'] === $url && $part['text'] === check_plain($title)); + } + // No parts must be left, or an expected "Home" will always pass. + $pass = ($pass && empty($parts)); + + $this->assertTrue($pass, t('Breadcrumb %parts found on @path.', array( + '%parts' => implode(' » ', $trail), + '@path' => $this->getUrl(), + ))); + + // Additionally assert page title, if given. + if (isset($page_title)) { + $this->assertTitle(strtr('@title | Drupal', array('@title' => $page_title))); + } + + // Additionally assert active trail in a menu tree output, if given. + if ($tree) { + end($tree); + $active_link_path = key($tree); + $active_link_title = array_pop($tree); + $xpath = ''; + if ($tree) { + $i = 0; + foreach ($tree as $link_path => $link_title) { + $part_xpath = (!$i ? '//' : '/following-sibling::ul/descendant::'); + $part_xpath .= 'li[contains(@class, :class)]/a[contains(@href, :href) and contains(text(), :title)]'; + $part_args = array( + ':class' => 'active-trail', + ':href' => url($link_path), + ':title' => $link_title, + ); + $xpath .= $this->buildXPathQuery($part_xpath, $part_args); + $i++; + } + $elements = $this->xpath($xpath); + $this->assertTrue(!empty($elements), t('Active trail to current page was found in menu tree.')); + + // Append prefix for active link asserted below. + $xpath .= '/following-sibling::ul/descendant::'; + } + else { + $xpath .= '//'; + } + $last_active = ($last_active ? 'and contains(@class, :class-active)' : ''); + $xpath .= 'li[contains(@class, :class-trail)]/a[contains(@href, :href) ' . $last_active . 'and contains(text(), :title)]'; + $args = array( + ':class-trail' => 'active-trail', + ':class-active' => 'active', + ':href' => url($active_link_path), + ':title' => $active_link_title, + ); + $elements = $this->xpath($xpath, $args); + $this->assertTrue(!empty($elements), t('Active link %title was found in menu tree, including active trail links %tree.', array( + '%title' => $active_link_title, + '%tree' => implode(' » ', $tree), + ))); + } + } + + /** + * Returns the breadcrumb contents of the current page in the internal browser. + */ + protected function getParts() { + $parts = array(); + $elements = $this->xpath('//div[@class="breadcrumb"]/a'); + if (!empty($elements)) { + foreach ($elements as $element) { + $parts[] = array( + 'text' => (string) $element, + 'href' => (string) $element['href'], + 'title' => (string) $element['title'], + ); + } + } + return $parts; + } +} diff --git modules/simpletest/tests/menu_test.module modules/simpletest/tests/menu_test.module index 4f84b15..b2322b5 100644 --- modules/simpletest/tests/menu_test.module +++ modules/simpletest/tests/menu_test.module @@ -77,7 +77,7 @@ function menu_test_menu() { 'access arguments' => array('access content'), ); $items['menu-test/hidden'] = array( - 'title' => 'Menu test parent', + 'title' => 'Hidden test root', 'page callback' => 'node_page_default', 'access arguments' => array('access content'), ); @@ -110,7 +110,6 @@ function menu_test_menu() { 'title' => 'Customize menu', 'page callback' => 'node_page_default', 'access arguments' => array('access content'), - 'type' => MENU_CALLBACK, ); $items['menu-test/hidden/menu/manage/%menu/list'] = array( 'title' => 'List links', @@ -135,7 +134,6 @@ function menu_test_menu() { 'title' => 'Delete menu', 'page callback' => 'node_page_default', 'access arguments' => array('access content'), - 'type' => MENU_CALLBACK, ); // Hidden tests; two dynamic arguments. @@ -173,6 +171,41 @@ function menu_test_menu() { 'context' => MENU_CONTEXT_NONE, ); + // Breadcrumbs tests. + // @see MenuBreadcrumbTestCase + $base = array( + 'page callback' => 'menu_test_callback', + 'access callback' => TRUE, + ); + // Local tasks: Second level below default local task. + $items['menu-test/breadcrumb/tasks'] = array( + 'title' => 'Breadcrumbs test: Local tasks', + ) + $base; + $items['menu-test/breadcrumb/tasks/first'] = array( + 'title' => 'First', + 'type' => MENU_DEFAULT_LOCAL_TASK, + ) + $base; + $items['menu-test/breadcrumb/tasks/second'] = array( + 'title' => 'Second', + 'type' => MENU_LOCAL_TASK, + ) + $base; + $items['menu-test/breadcrumb/tasks/first/first'] = array( + 'title' => 'First first', + 'type' => MENU_DEFAULT_LOCAL_TASK, + ) + $base; + $items['menu-test/breadcrumb/tasks/first/second'] = array( + 'title' => 'First second', + 'type' => MENU_LOCAL_TASK, + ) + $base; + $items['menu-test/breadcrumb/tasks/second/first'] = array( + 'title' => 'Second first', + 'type' => MENU_DEFAULT_LOCAL_TASK, + ) + $base; + $items['menu-test/breadcrumb/tasks/second/second'] = array( + 'title' => 'Second second', + 'type' => MENU_LOCAL_TASK, + ) + $base; + // File inheritance tests. This menu item should inherit the page callback // system_admin_menu_block_page() and therefore render its children as links // on the page. diff --git modules/statistics/statistics.module modules/statistics/statistics.module index 01f4de7..1959dc4 100644 --- modules/statistics/statistics.module +++ modules/statistics/statistics.module @@ -163,7 +163,6 @@ function statistics_menu() { 'page callback' => 'statistics_access_log', 'page arguments' => array(3), 'access arguments' => array('access statistics'), - 'type' => MENU_CALLBACK, 'file' => 'statistics.admin.inc', ); $items['admin/config/system/statistics'] = array( diff --git modules/statistics/statistics.test modules/statistics/statistics.test index 6a9da4f..b73b1b7 100644 --- modules/statistics/statistics.test +++ modules/statistics/statistics.test @@ -10,7 +10,15 @@ class StatisticsTestCase extends DrupalWebTestCase { parent::setUp('statistics'); // Create user. - $this->blocking_user = $this->drupalCreateUser(array('block IP addresses', 'access statistics', 'administer blocks', 'administer statistics', 'administer users')); + $this->blocking_user = $this->drupalCreateUser(array( + 'access administration pages', + 'access site reports', + 'access statistics', + 'block IP addresses', + 'administer blocks', + 'administer statistics', + 'administer users', + )); $this->drupalLogin($this->blocking_user); // Enable access logging. diff --git modules/system/system.api.php modules/system/system.api.php index d8450db..7dbbe30 100644 --- modules/system/system.api.php +++ modules/system/system.api.php @@ -1200,6 +1200,53 @@ function hook_menu_local_tasks_alter(&$data, $router_item, $root_path) { } /** + * Alter links in the active trail (breadcrumb) before it is rendered. + * + * This hook is invoked by menu_get_active_breadcrumb() and allows to alter + * the breadcrumb links for the current page, which may be preferred instead + * of setting a custom breadcrumb via drupal_set_breadcrumb(). + * + * Implementations should take into account that menu_get_active_breadcrumb() + * subsequently performs the following adjustments to the active trail *after* + * this hook has been invoked: + * - The last link in $active_trail is removed, if its 'href' is identical to + * the 'href' of $item. This happens, because the breadcrumb normally does + * not contain a link to the current page. + * - The (second to) last link in $active_trail is removed, if the current $item + * is a MENU_DEFAULT_LOCAL_TASK. This happens in order to do not show a link + * to the current page, when being on the path for the default local task; + * e.g. when being on the path node/%/view, the breadcrumb should not contain + * a link to node/%. + * + * Each link in the active trail must contain: + * - title: The localized title of the link. + * - href: The system path to link to. + * - localized_options: An array of options to pass to url(). + * + * @param $active_trail + * An array containing breadcrumb links for the current page. + * @param $item + * The menu router item of the current page. + * + * @see drupal_set_breadcrumb() + * @see menu_get_active_breadcrumb() + * @see menu_get_active_trail() + * @see menu_set_active_trail() + */ +function hook_menu_breadcrumb_alter(&$active_trail, $item) { + // Always display a link to the current page by duplicating the last link in + // the active trail. This means that menu_get_active_breadcrumb() will remove + // the last link (for the current page), but since it is added once more here, + // it will appear. + if (!drupal_is_front_page()) { + $end = end($active_trail); + if ($item['href'] == $end['href']) { + $active_trail[] = $end; + } + } +} + +/** * Alter contextual links before they are rendered. * * This hook is invoked by menu_contextual_links(). The system-determined diff --git modules/system/system.module modules/system/system.module index ce489a5..849c079 100644 --- modules/system/system.module +++ modules/system/system.module @@ -648,7 +648,6 @@ function system_menu() { $items['admin/modules/list/confirm'] = array( 'title' => 'List', 'access arguments' => array('administer modules'), - 'type' => MENU_CALLBACK, ); $items['admin/modules/uninstall'] = array( 'title' => 'Uninstall', @@ -661,7 +660,6 @@ function system_menu() { $items['admin/modules/uninstall/confirm'] = array( 'title' => 'Uninstall', 'access arguments' => array('administer modules'), - 'type' => MENU_CALLBACK, 'file' => 'system.admin.inc', ); @@ -688,7 +686,6 @@ function system_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('system_ip_blocking_delete', 5), 'access arguments' => array('block IP addresses'), - 'type' => MENU_CALLBACK, 'file' => 'system.admin.inc', ); @@ -829,7 +826,6 @@ function system_menu() { $items['admin/config/regional/date-time/types/%/delete'] = array( 'title' => 'Delete date type', 'description' => 'Allow users to delete a configured date type.', - 'type' => MENU_CALLBACK, 'page callback' => 'drupal_get_form', 'page arguments' => array('system_delete_date_format_type_form', 5), 'access arguments' => array('administer site configuration'), @@ -857,7 +853,6 @@ function system_menu() { $items['admin/config/regional/date-time/formats/%/edit'] = array( 'title' => 'Edit date format', 'description' => 'Allow users to edit a configured date format.', - 'type' => MENU_CALLBACK, 'page callback' => 'drupal_get_form', 'page arguments' => array('system_configure_date_formats_form', 5), 'access arguments' => array('administer site configuration'), @@ -866,7 +861,6 @@ function system_menu() { $items['admin/config/regional/date-time/formats/%/delete'] = array( 'title' => 'Delete date format', 'description' => 'Allow users to delete a configured date format.', - 'type' => MENU_CALLBACK, 'page callback' => 'drupal_get_form', 'page arguments' => array('system_date_delete_format_form', 5), 'access arguments' => array('administer site configuration'), @@ -874,7 +868,6 @@ function system_menu() { ); $items['admin/config/regional/date-time/formats/lookup'] = array( 'title' => 'Date and time lookup', - 'type' => MENU_CALLBACK, 'page callback' => 'system_date_time_lookup', 'access arguments' => array('administer site configuration'), 'file' => 'system.admin.inc', @@ -938,7 +931,6 @@ function system_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('system_actions_configure'), 'access arguments' => array('administer actions'), - 'type' => MENU_CALLBACK, 'file' => 'system.admin.inc', ); $items['admin/config/system/actions/delete/%actions'] = array( @@ -947,7 +939,6 @@ function system_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('system_actions_delete_form', 5), 'access arguments' => array('administer actions'), - 'type' => MENU_CALLBACK, 'file' => 'system.admin.inc', ); $items['admin/config/system/actions/orphan'] = array( diff --git modules/system/system.test modules/system/system.test index 3cc68d6..c2c1aae 100644 --- modules/system/system.test +++ modules/system/system.test @@ -653,8 +653,7 @@ class AccessDeniedTestCase extends DrupalWebTestCase { parent::setUp(); // Create an administrative user. - $this->admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer blocks')); - $this->drupalLogin($this->admin_user); + $this->admin_user = $this->drupalCreateUser(array('access administration pages', 'administer site configuration', 'administer blocks')); } function testAccessDenied() { @@ -662,6 +661,7 @@ class AccessDeniedTestCase extends DrupalWebTestCase { $this->assertText(t('Access denied'), t('Found the default 403 page')); $this->assertResponse(403); + $this->drupalLogin($this->admin_user); $edit = array( 'title' => $this->randomName(10), 'body' => array(LANGUAGE_NONE => array(array('value' => $this->randomName(100)))), @@ -671,6 +671,7 @@ class AccessDeniedTestCase extends DrupalWebTestCase { // Use a custom 403 page. $this->drupalPost('admin/config/system/site-information', array('site_403' => 'node/' . $node->nid), t('Save configuration')); + $this->drupalLogout(); $this->drupalGet('admin'); $this->assertText($node->title, t('Found the custom 403 page')); diff --git modules/taxonomy/taxonomy.module modules/taxonomy/taxonomy.module index d220848..193723c 100644 --- modules/taxonomy/taxonomy.module +++ modules/taxonomy/taxonomy.module @@ -287,7 +287,6 @@ function taxonomy_menu() { 'page callback' => 'taxonomy_term_page', 'page arguments' => array(2), 'access arguments' => array('access content'), - 'type' => MENU_CALLBACK, 'file' => 'taxonomy.pages.inc', ); $items['taxonomy/term/%taxonomy_term/view'] = array( diff --git modules/taxonomy/taxonomy.pages.inc modules/taxonomy/taxonomy.pages.inc index 43509ed..12408f7 100644 --- modules/taxonomy/taxonomy.pages.inc +++ modules/taxonomy/taxonomy.pages.inc @@ -19,6 +19,8 @@ function taxonomy_term_page($term) { $current = (object) array( 'tid' => $term->tid, ); + // @todo This overrides any other possible breadcrumb and is a pure hard-coded + // presumption. Make this behavior configurable per vocabulary or term. $breadcrumb = array(); while ($parents = taxonomy_get_parents($current->tid)) { $current = array_shift($parents); diff --git modules/taxonomy/taxonomy.test modules/taxonomy/taxonomy.test index 5e96104..61500e5 100644 --- modules/taxonomy/taxonomy.test +++ modules/taxonomy/taxonomy.test @@ -557,7 +557,7 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase { // the first edit link found on the listing page is to our term. $this->clickLink(t('edit')); - $this->assertText($edit['name'], t('The randomly generated term name is present.')); + $this->assertRaw($edit['name'], t('The randomly generated term name is present.')); $this->assertText($edit['description[value]'], t('The randomly generated term description is present.')); $edit = array( @@ -939,7 +939,7 @@ class TaxonomyTermFieldTestCase extends TaxonomyWebTestCase { "{$this->field_name}[$langcode]" => array($term->tid), ); $this->drupalPost(NULL, $edit, t('Save')); - preg_match('|test-entity/(\d+)/edit|', $this->url, $match); + preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match); $id = $match[1]; $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), t('Entity was created')); diff --git modules/trigger/trigger.module modules/trigger/trigger.module index 808bb90..db19ee7 100644 --- modules/trigger/trigger.module +++ modules/trigger/trigger.module @@ -80,7 +80,6 @@ function trigger_menu() { 'page callback' => 'drupal_get_form', 'page arguments' => array('trigger_unassign'), 'access arguments' => array('administer actions'), - 'type' => MENU_CALLBACK, 'file' => 'trigger.admin.inc', ); diff --git modules/user/user.module modules/user/user.module index 2e84c6e..062798d 100644 --- modules/user/user.module +++ modules/user/user.module @@ -1526,7 +1526,12 @@ function user_menu() { 'title' => 'User account', 'page callback' => 'user_page', 'access callback' => TRUE, - 'type' => MENU_CALLBACK, + // Edge-case: No menu links should be auto-generated for this and below + // items, which makes it a MENU_CALLBACK. However, this item's title is + // expected to appear on user login, register, and password pages, so we + // need to use MENU_VISIBLE_IN_BREADCRUMB to make + // menu_get_active_breadcrumb() account for it. + 'type' => MENU_VISIBLE_IN_BREADCRUMB, 'file' => 'user.pages.inc', ); @@ -1621,7 +1626,6 @@ function user_menu() { 'page arguments' => array('user_admin_role', 5), 'access callback' => 'user_role_edit_access', 'access arguments' => array(5), - 'type' => MENU_CALLBACK, ); $items['admin/people/permissions/roles/delete/%user_role'] = array( 'title' => 'Delete role', @@ -1629,7 +1633,6 @@ function user_menu() { 'page arguments' => array('user_admin_role_delete_confirm', 5), 'access callback' => 'user_role_edit_access', 'access arguments' => array(5), - 'type' => MENU_CALLBACK, 'file' => 'user.admin.inc', ); @@ -1692,7 +1695,6 @@ function user_menu() { 'page arguments' => array('user_cancel_confirm_form', 1), 'access callback' => 'user_cancel_access', 'access arguments' => array(1), - 'type' => MENU_CALLBACK, 'file' => 'user.pages.inc', ); @@ -1702,7 +1704,6 @@ function user_menu() { 'page arguments' => array(1, 4, 5), 'access callback' => 'user_cancel_access', 'access arguments' => array(1), - 'type' => MENU_CALLBACK, 'file' => 'user.pages.inc', );