diff --git a/core/core.services.yml b/core/core.services.yml index 1b0d520..80f30e1 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -530,6 +530,7 @@ services: arguments: ['@image.toolkit'] breadcrumb: class: Drupal\Core\Breadcrumb\BreadcrumbManager + arguments: ['@module_handler'] token: class: Drupal\Core\Utility\Token arguments: ['@module_handler'] diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 46ed01d..3ba3b5d 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -146,7 +146,7 @@ */ /** - * Menu type -- A "normal" menu item that's shown in menu and breadcrumbs. + * Menu type -- A "normal" menu item that's shown in menus. * * Normal menu items show up in the menu tree and can be moved/hidden by * the administrator. Use this for most menu items. It is the default value if @@ -158,7 +158,7 @@ * 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 do not appear in menus or breadcrumbs. + * when the URL is accessed. They do not appear in menus. */ const MENU_CALLBACK = 0x0000; @@ -200,7 +200,7 @@ /** * Menu type -- A task specific to the parent, which is never rendered. * - * Sibling local tasks are not rendered themselves, but affect the breadcrumb + * Sibling local tasks are not rendered themselves, but affect the active * trail and need their sibling tasks rendered as tabs. */ define('MENU_SIBLING_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_IS_LOCAL_ACTION | MENU_VISIBLE_IN_BREADCRUMB); @@ -437,7 +437,7 @@ function menu_unserialize($data, $map) { * @param $router_item * The router item. Usually a router entry from menu_get_item() is either * modified or set to a different path. This allows the navigation block, - * the page title, the breadcrumb, and the page help to be modified in one + * the page title, the active trail, and the page help to be modified in one * call. */ function menu_set_item($path, $router_item) { @@ -898,7 +898,6 @@ function _menu_link_translate(&$item, $translate = FALSE) { // 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. @@ -1281,7 +1280,7 @@ function menu_tree_get_path($menu_name) { * @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. + * to FALSE. * * @return * An array of menu links, in the order they should be rendered. The array @@ -1328,7 +1327,7 @@ function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail = // template_preprocess_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. + // tree containing the active trail only. // @see menu_set_active_trail() if (!isset($tree[$cid]) && $only_active_trail) { $cid .= ':trail'; @@ -1430,8 +1429,7 @@ function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail = * - 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. + * trail. This option is ignored, if 'expanded' is non-empty. * - 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 * (excluding menu container itself). @@ -1641,7 +1639,7 @@ function _menu_tree_data(&$links, $parents, $depth) { $tree = array(); while ($item = array_pop($links)) { // We need to determine if we're on the path to root so we can later build - // the correct active trail and breadcrumb. + // the correct active trail. $item['in_active_trail'] = in_array($item['mlid'], $parents); // Add the current link to the tree. $tree[$item['mlid']] = array( @@ -2441,9 +2439,7 @@ function menu_set_active_item($path) { * * Any trail set by this function will only be used for functionality that calls * menu_get_active_trail(). Drupal core only uses trails set here for - * breadcrumbs and the page title and not for menu trees or page content. - * Additionally, breadcrumbs set by drupal_set_breadcrumb() will override any - * trail set here. + * the page title and not for menu trees or page content. * * To affect the trail used by menu trees, use menu_tree_set_path(). To affect * the page content, use menu_set_active_item() instead. @@ -2640,57 +2636,6 @@ function menu_get_active_trail() { } /** - * Gets the breadcrumb for the current page, as determined by the active trail. - * - * @see menu_set_active_trail() - */ -function menu_get_active_breadcrumb() { - $breadcrumb = array(); - - // No breadcrumb for the front page. - if (drupal_is_front_page()) { - return $breadcrumb; - } - - $item = menu_get_item(); - if (!empty($item['access'])) { - $active_trail = menu_get_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. - $end = end($active_trail); - if (\Drupal::request()->attributes->get('_system_path') == $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; -} - -/** * Gets the title of the current page, as determined by the active trail. */ function menu_get_active_title() { diff --git a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php index 5396e9e..d38449c 100644 --- a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php +++ b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php @@ -7,6 +7,8 @@ namespace Drupal\Core\Breadcrumb; +use Drupal\Core\Extension\ModuleHandlerInterface; + /** * Provides a breadcrumb manager. * @@ -16,6 +18,13 @@ class BreadcrumbManager implements BreadcrumbBuilderInterface { /** + * The module handler to invoke the alter hook. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** * Holds arrays of breadcrumb builders, keyed by priority. * * @var array @@ -32,6 +41,16 @@ class BreadcrumbManager implements BreadcrumbBuilderInterface { protected $sortedBuilders; /** + * Constructs a \Drupal\Core\Breadcrumb\BreadcrumbManager object. + * + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + */ + public function __construct(ModuleHandlerInterface $module_handler) { + $this->moduleHandler = $module_handler; + } + + /** * Adds another breadcrumb builder. * * @param \Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface $builder @@ -49,25 +68,30 @@ public function addBuilder(BreadcrumbBuilderInterface $builder, $priority) { * {@inheritdoc} */ public function build(array $attributes) { + $breadcrumb = array(); + $context = array('builder' => NULL); // Call the build method of registered breadcrumb builders, // until one of them returns an array. foreach ($this->getSortedBuilders() as $builder) { - $breadcrumb = $builder->build($attributes); - if (!isset($breadcrumb)) { + $build = $builder->build($attributes); + if (!isset($build)) { // The builder returned NULL, so we continue with the other builders. continue; } - elseif (is_array($breadcrumb)) { + elseif (is_array($build)) { // The builder returned an array of breadcrumb links. - return $breadcrumb; + $breadcrumb = $build; + $context['builder'] = $builder; + break; } else { throw new \UnexpectedValueException(format_string('Invalid breadcrumb returned by !class::build().', array('!class' => get_class($builder)))); } } - + // Allow modules to alter the breadcrumb. + $this->moduleHandler->alter('system_breadcrumb', $breadcrumb, $attributes, $context); // Fall back to an empty breadcrumb. - return array(); + return $breadcrumb; } /** diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php index b03cc8e..2d8adf0 100644 --- a/core/lib/Drupal/Core/Controller/ExceptionController.php +++ b/core/lib/Drupal/Core/Controller/ExceptionController.php @@ -91,7 +91,7 @@ public function on403Html(FlattenException $exception, Request $request) { $request->query->set('destination', $system_path); } - $subrequest = Request::create('/' . $path, 'get', array('destination' => $system_path), $request->cookies->all(), array(), $request->server->all()); + $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'get', array('destination' => $system_path), $request->cookies->all(), array(), $request->server->all()); // The active trail is being statically cached from the parent request to // the subrequest, like any other static. Unfortunately that means the @@ -164,7 +164,7 @@ public function on404Html(FlattenException $exception, Request $request) { // that and sub-call the kernel rather than using meah(). // @todo The create() method expects a slash-prefixed path, but we store a // normal system path in the site_404 variable. - $subrequest = Request::create('/' . $path, 'get', array(), $request->cookies->all(), array(), $request->server->all()); + $subrequest = Request::create($request->getBaseUrl() . '/' . $path, 'get', array('destination' => $system_path), $request->cookies->all(), array(), $request->server->all()); // The active trail is being statically cached from the parent request to // the subrequest, like any other static. Unfortunately that means the diff --git a/core/modules/book/book.services.yml b/core/modules/book/book.services.yml index 2f1097a..f7ff964 100644 --- a/core/modules/book/book.services.yml +++ b/core/modules/book/book.services.yml @@ -1,4 +1,9 @@ services: + book.breadcrumb: + class: Drupal\book\BookBreadcrumbBuilder + arguments: ['@entity.manager', '@string_translation', '@link_generator', '@access_manager'] + tags: + - { name: breadcrumb_builder, priority: 701 } book.manager: class: Drupal\book\BookManager arguments: ['@database', '@entity.manager', '@string_translation', '@config.factory'] diff --git a/core/modules/book/lib/Drupal/book/BookBreadcrumbBuilder.php b/core/modules/book/lib/Drupal/book/BookBreadcrumbBuilder.php new file mode 100644 index 0000000..2fef927 --- /dev/null +++ b/core/modules/book/lib/Drupal/book/BookBreadcrumbBuilder.php @@ -0,0 +1,108 @@ +menuLinkStorage = $entity_manager->getStorageController('menu_link'); + $this->translation = $translation; + $this->linkGenerator = $link_generator; + $this->accessManager = $access_manager; + } + + /** + * {@inheritdoc} + */ + public function build(array $attributes) { + if (!empty($attributes['node']) && $attributes['node'] instanceof NodeInterface && !empty($attributes['node']->book)) { + $mlids = array(); + $links = array($this->linkGenerator->generate($this->t('Home'), '')); + $book = $attributes['node']->book; + $depth = 1; + // We skip the current node. + while (!empty($book['p' . ($depth + 1)])) { + $mlids[] = $book['p' . $depth]; + $depth++; + } + $menu_links = $this->menuLinkStorage->loadMultiple($mlids); + if (count($menu_links) > 0) { + $depth = 1; + while (!empty($book['p' . ($depth + 1)])) { + if (!empty($menu_links[$book['p' . $depth]]) && ($menu_link = $menu_links[$book['p' . $depth]])) { + if ($this->accessManager->checkNamedRoute($menu_link->route_name, $menu_link->route_parameters)) { + $links[] = $this->linkGenerator->generate($menu_link->label(), $menu_link->route_name, $menu_link->route_parameters, $menu_link->options); + } + } + $depth++; + } + } + return $links; + } + } + + /** + * Translates a string to the current language or to a given language. + * + * See the t() documentation for details. + */ + protected function t($string, array $args = array(), array $options = array()) { + return $this->translation->translate($string, $args, $options); + } + +} diff --git a/core/modules/field_ui/lib/Drupal/field_ui/Routing/RouteSubscriber.php b/core/modules/field_ui/lib/Drupal/field_ui/Routing/RouteSubscriber.php index 490a0d0..456e1fd 100644 --- a/core/modules/field_ui/lib/Drupal/field_ui/Routing/RouteSubscriber.php +++ b/core/modules/field_ui/lib/Drupal/field_ui/Routing/RouteSubscriber.php @@ -81,14 +81,20 @@ public function routes(RouteBuildEvent $event) { } $route = new Route( "$path/fields", - array('_form' => '\Drupal\field_ui\FieldOverview') + $defaults, + array( + '_form' => '\Drupal\field_ui\FieldOverview', + '_title' => 'Manage fields', + ) + $defaults, array('_permission' => 'administer ' . $entity_type . ' fields') ); $collection->add("field_ui.overview_$entity_type", $route); $route = new Route( "$path/form-display", - array('_form' => '\Drupal\field_ui\FormDisplayOverview') + $defaults, + array( + '_form' => '\Drupal\field_ui\FormDisplayOverview', + '_title' => 'Manage form display', + ) + $defaults, array('_permission' => 'administer ' . $entity_type . ' form display') ); $collection->add("field_ui.form_display_overview_$entity_type", $route); @@ -106,7 +112,10 @@ public function routes(RouteBuildEvent $event) { $route = new Route( "$path/display", - array('_form' => '\Drupal\field_ui\DisplayOverview') + $defaults, + array( + '_form' => '\Drupal\field_ui\DisplayOverview', + '_title' => 'Manage display', + ) + $defaults, array('_permission' => 'administer ' . $entity_type . ' display') ); $collection->add("field_ui.display_overview_$entity_type", $route); diff --git a/core/modules/filter/filter.routing.yml b/core/modules/filter/filter.routing.yml index 93bb987..88e2711 100644 --- a/core/modules/filter/filter.routing.yml +++ b/core/modules/filter/filter.routing.yml @@ -17,6 +17,7 @@ filter.admin_overview: defaults: _content: '\Drupal\Core\Entity\Controller\EntityListController::listing' entity_type: 'filter_format' + _title: 'Text formats and editors' requirements: _permission: 'administer filters' diff --git a/core/modules/image/lib/Drupal/image/PathProcessor/PathProcessorImageStyles.php b/core/modules/image/lib/Drupal/image/PathProcessor/PathProcessorImageStyles.php index c0c6f31..4418476 100644 --- a/core/modules/image/lib/Drupal/image/PathProcessor/PathProcessorImageStyles.php +++ b/core/modules/image/lib/Drupal/image/PathProcessor/PathProcessorImageStyles.php @@ -46,12 +46,17 @@ public function processInbound($path, Request $request) { $rest = preg_replace('|^' . $path_prefix . '|', '', $path); // Get the image style, scheme and path. - list($image_style, $scheme, $file) = explode('/', $rest, 3); + if (substr_count($rest, '/') >= 2) { + list($image_style, $scheme, $file) = explode('/', $rest, 3); - // Set the file as query parameter. - $request->query->set('file', $file); + // Set the file as query parameter. + $request->query->set('file', $file); - return $path_prefix . $image_style . '/' . $scheme; + return $path_prefix . $image_style . '/' . $scheme; + } + else { + return $path; + } } } diff --git a/core/modules/menu/menu.routing.yml b/core/modules/menu/menu.routing.yml index aaee350..21743dd 100644 --- a/core/modules/menu/menu.routing.yml +++ b/core/modules/menu/menu.routing.yml @@ -9,6 +9,7 @@ menu.overview_page: path: '/admin/structure/menu' defaults: _entity_list: 'menu' + _title: 'Menus' requirements: _permission: 'administer menu' diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkBreadcrumbBuilder.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkBreadcrumbBuilder.php deleted file mode 100644 index acece08..0000000 --- a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkBreadcrumbBuilder.php +++ /dev/null @@ -1,26 +0,0 @@ -isNew()) { - // Get the human-readable menu title from the given menu name. - $titles = menu_get_menus(); - $current_title = $titles[$menu_link->menu_name]; - - // Get the current breadcrumb and add a link to that menu's overview page. - $breadcrumb = menu_get_active_breadcrumb(); - $breadcrumb[] = l($current_title, 'admin/structure/menu/manage/' . $menu_link->menu_name); - drupal_set_breadcrumb($breadcrumb); - } - $form['link_title'] = array( '#type' => 'textfield', '#title' => t('Menu link title'), diff --git a/core/modules/menu_link/menu_link.module b/core/modules/menu_link/menu_link.module index 85d8e92..7e0358e 100644 --- a/core/modules/menu_link/menu_link.module +++ b/core/modules/menu_link/menu_link.module @@ -6,6 +6,8 @@ */ use Drupal\menu_link\Entity\MenuLink; +use Drupal\menu_link\MenuLinkInterface; +use Symfony\Cmf\Component\Routing\RouteObjectInterface; function menu_link_help($path, $arg) { switch ($path) { @@ -195,3 +197,19 @@ function menu_link_maintain($module, $op, $link_path, $link_title = NULL) { break; } } + +/** + * Implements hook_system_breadcrumb_alter(). + */ +function menu_link_system_breadcrumb_alter(array &$breadcrumb, array $attributes, array $context) { + // Custom breadcrumb behaviour for editing menu links, we append a link to + // the menu in which the link is found. + if (!empty($attributes[RouteObjectInterface::ROUTE_NAME]) && $attributes[RouteObjectInterface::ROUTE_NAME] == 'menu.link_edit' && !empty($attributes['menu_link'])) { + $menu_link = $attributes['menu_link']; + if (($menu_link instanceof MenuLinkInterface) && !$menu_link->isNew()) { + // Add a link to the menu admin screen. + $menu = entity_load('menu', $menu_link->menu_name); + $breadcrumb[] = Drupal::l($menu->label(), 'menu.menu_edit', array('menu' => $menu->id)); + } + } +} diff --git a/core/modules/menu_link/menu_link.services.yml b/core/modules/menu_link/menu_link.services.yml deleted file mode 100644 index a428476..0000000 --- a/core/modules/menu_link/menu_link.services.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - menu_link.breadcrumb: - class: Drupal\menu_link\MenuLinkBreadcrumbBuilder - tags: - - { name: breadcrumb_builder, priority: 0 } diff --git a/core/modules/node/lib/Drupal/node/Controller/NodeController.php b/core/modules/node/lib/Drupal/node/Controller/NodeController.php index fdc41e6..d1ea5da 100644 --- a/core/modules/node/lib/Drupal/node/Controller/NodeController.php +++ b/core/modules/node/lib/Drupal/node/Controller/NodeController.php @@ -32,13 +32,6 @@ public function add(EntityInterface $node_type) { } /** - * @todo Remove node_page_view(). - */ - public function viewPage(NodeInterface $node) { - return node_page_view($node); - } - - /** * @todo Remove node_show(). */ public function revisionShow($node_revision) { diff --git a/core/modules/node/lib/Drupal/node/Controller/NodeView.php b/core/modules/node/lib/Drupal/node/Controller/NodeView.php new file mode 100644 index 0000000..33181d8 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Controller/NodeView.php @@ -0,0 +1,35 @@ +label()); + } + +} diff --git a/core/modules/node/node.module b/core/modules/node/node.module index e1d2b63..cf31cc4 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1477,10 +1477,6 @@ function node_page_view(EntityInterface $node) { $build = node_show($node); - // If there is a menu link to this node, the link becomes the last part - // of the active trail, and the link name becomes the page title. - // Thus, we must explicitly set the page title to be the node title. - $build['#title'] = String::checkPlain($node->label()); return $build; } diff --git a/core/modules/node/node.routing.yml b/core/modules/node/node.routing.yml index c2a4ff1..2f8c8ab 100644 --- a/core/modules/node/node.routing.yml +++ b/core/modules/node/node.routing.yml @@ -31,7 +31,8 @@ node.add: node.view: path: '/node/{node}' defaults: - _content: '\Drupal\node\Controller\NodeController::viewPage' + _content: '\Drupal\node\Controller\NodeView::page' + _title_callback: '\Drupal\node\Controller\NodeView::pageTitle' requirements: _entity_access: 'node.view' @@ -77,6 +78,7 @@ node.overview_types: defaults: _content: '\Drupal\Core\Entity\Controller\EntityListController::listing' entity_type: 'node_type' + _title: 'Content types' requirements: _permission: 'administer content types' diff --git a/core/modules/system/lib/Drupal/system/PathBasedBreadcrumbBuilder.php b/core/modules/system/lib/Drupal/system/PathBasedBreadcrumbBuilder.php new file mode 100644 index 0000000..1279a92 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/PathBasedBreadcrumbBuilder.php @@ -0,0 +1,239 @@ +request = $request; + $this->accessManager = $access_manager; + $this->translation = $translation; + $this->menuStorage = $entity_manager->getStorageController('menu'); + $this->router = $router; + $this->pathProcessor = $path_processor; + $this->config = $config_factory->get('system.site'); + $this->linkGenerator = $link_generator; + $this->titleResolver = $title_resolver; + } + + /** + * {@inheritdoc} + */ + public function build(array $attributes) { + $links = array(); + + // General path-based breadcrumbs. Use the actual request path, prior to + // resolving path aliases, so the breadcrumb can be defined by simply + // creating a hierarchy of path aliases. + $path = trim($this->request->getPathInfo(), '/'); + $path_elements = explode('/', $path); + $exclude = array(); + // Don't show a link to the front-page path. + $front = $this->config->get('page.front'); + $exclude[$front] = TRUE; + // /user is just a redirect, so skip it. + // @todo Find a better way to deal with /user. + $exclude['user'] = TRUE; + while (count($path_elements) > 1) { + array_pop($path_elements); + // Copy the path elements for up-casting. + $route_request = $this->getRequestForPath(implode('/', $path_elements), $exclude); + if ($route_request) { + if (!$route_request->attributes->get('_legacy')) { + $route_name = $route_request->attributes->get(RouteObjectInterface::ROUTE_NAME); + // Note that the parameters don't really matter here since we're + // passing in the request which already has the upcast attributes. + $parameters = array(); + $access = $this->accessManager->checkNamedRoute($route_name, $parameters, $route_request); + if ($access) { + $title = $this->titleResolver->getTitle($route_request, $route_request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)); + } + } + // @todo - remove this once all of core is converted to the new router. + else { + $menu_item = $route_request->attributes->get('_drupal_menu_item'); + // Skip the breadcrumb step for menu items linking to the parent item. + if (($menu_item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) { + continue; + } + $access = $menu_item['access']; + $title = $menu_item['title']; + } + if ($access) { + if (!$title) { + // Fallback to using the raw path component as the title if the + // route is missing a _title or _title_callback attribute. + $title = str_replace(array('-', '_'), ' ', Unicode::ucfirst(end($path_elements))); + } + // @todo Replace with a #type => link render element so that the alter + // hook can work with the actual data. + $links[] = l($title, $route_request->attributes->get('_system_path')); + } + } + + } + if ($path && $path != $front) { + // Add the Home link, except for the front page. + $links[] = $this->linkGenerator->generate($this->t('Home'), ''); + } + return array_reverse($links); + } + + /** + * Matches a path in the router. + * + * @param string $path + * The request path. + * @param array $exclude + * An array of paths or system paths to skip. + * + * @return \Symfony\Component\HttpFoundation\Request + * A populated request object or NULL if the patch couldn't be matched. + */ + protected function getRequestForPath($path, array $exclude) { + if (!empty($exclude[$path])) { + return NULL; + } + // @todo Use the RequestHelper once https://drupal.org/node/2090293 is + // fixed. + $request = Request::create($this->request->getBaseUrl() . '/' . $path); + // Find the system path by resolving aliases, language prefix, etc. + $processed = $this->pathProcessor->processInbound($path, $request); + if (empty($processed) || !empty($exclude[$processed])) { + // This resolves to the front page, which we already add. + return NULL; + } + $request->attributes->set('_system_path', $processed); + // Attempt to match this path to provide a fully built request. + try { + $request->attributes->add($this->router->matchRequest($request)); + return $request; + } + catch (NotFoundHttpException $e) { + return NULL; + } + catch (ResourceNotFoundException $e) { + return NULL; + } + } + + /** + * Translates a string to the current language or to a given language. + * + * See the t() documentation for details. + */ + protected function t($string, array $args = array(), array $options = array()) { + return $this->translation->translate($string, $args, $options); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php index 23e8cf9..c4a1299 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php @@ -7,6 +7,8 @@ namespace Drupal\system\Tests\Menu; +use Drupal\Component\Utility\Unicode; + /** * Menu breadcrumbs related tests. */ @@ -56,34 +58,11 @@ function setUp() { */ function testBreadCrumbs() { // Prepare common base breadcrumb elements. - $home = array('' => 'Home'); + $home = array('' => 'Home'); $admin = $home + array('admin' => t('Administration')); $config = $admin + array('admin/config' => t('Configuration')); $type = 'article'; - // 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); - $this->assertBreadcrumb('menu-test/breadcrumb/tasks/first/first', $trail, $title, $tree); - $trail += array( - 'menu-test/breadcrumb/tasks' => t('Breadcrumbs test: Local tasks'), - ); - $this->assertBreadcrumb('menu-test/breadcrumb/tasks/first/second', $trail, $title, $tree); - $this->assertBreadcrumb('menu-test/breadcrumb/tasks/second', $trail, $title, $tree); - $this->assertBreadcrumb('menu-test/breadcrumb/tasks/second/first', $trail, $title, $tree); - $trail += array( - 'menu-test/breadcrumb/tasks/second' => t('Second'), - ); - $this->assertBreadcrumb('menu-test/breadcrumb/tasks/second/second', $trail, $title, $tree); - // Verify Taxonomy administration breadcrumbs. $trail = $admin + array( 'admin/structure' => t('Structure'), @@ -160,8 +139,10 @@ function testBreadCrumbs() { ); $this->assertBreadcrumb('admin/config/content/formats/add', $trail); $this->assertBreadcrumb("admin/config/content/formats/manage/$format_id", $trail); + // @todo Remove this part once we have a _title_callback, see + // https://drupal.org/node/2076085. $trail += array( - "admin/config/content/formats/manage/$format_id" => $format->name, + "admin/config/content/formats/manage/$format_id" => Unicode::ucfirst(Unicode::strtolower($format->name)), ); $this->assertBreadcrumb("admin/config/content/formats/manage/$format_id/disable", $trail); @@ -208,49 +189,9 @@ function testBreadCrumbs() { 'plid' => 0, )), )); - $nid2 = $node2->id(); - - $trail = $home; - $tree = array( - "node/$nid2" => $node2->menu['link_title'], - ); - $this->assertBreadcrumb("node/$nid2", $trail, $node2->getTitle(), $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' => entity_create('menu_link', array( - 'enabled' => 1, - 'link_title' => 'Child ' . $title, - 'description' => '', - 'menu_name' => $menu, - 'plid' => $node2->menu['mlid'], - )), - )); - $nid3 = $node3->id(); - - $this->assertBreadcrumb("node/$nid3", $trail, $node3->getTitle(), $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 == 'tools') { $parent = $node2; - $child = $node3; } } @@ -276,14 +217,9 @@ function testBreadCrumbs() { $tree = $expected + array( 'node/' . $parent->id() => $parent->menu['link_title'], ); - $this->assertBreadcrumb(NULL, $trail, $parent->getTitle(), $tree); $trail += array( 'node/' . $parent->id() => $parent->menu['link_title'], ); - $tree += array( - 'node/' . $parent->id() => $child->menu['link_title'], - ); - $this->assertBreadcrumb('node/' . $child->id(), $trail, $child->getTitle(), $tree); // Add a taxonomy term/tag to last node, and add a link for that term to the // Tools menu. @@ -443,38 +379,6 @@ function testBreadCrumbs() { ); // $this->assertBreadcrumb('user/' . $this->admin_user->id(), $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->id() . '/edit', $trail, $link_admin_user['link_title'], $tree, FALSE); - - // Move 'user/%' below 'user' and verify again. - $edit = array( - 'parent' => "$menu:{$link_user['mlid']}", - ); - $this->drupalPostForm("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->id(), $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->id() . '/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( @@ -483,16 +387,19 @@ function testBreadCrumbs() { $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(); + // page title, and that the breadcrumb is just the Home link (because the + // user is not able to access "Administer". + $trail = $home; $this->assertBreadcrumb('admin', $trail, t('Access denied')); $this->assertResponse(403); - $trail = $home; + // Since the 'admin' path is not accessible, we still expect only the Home + // link. $this->assertBreadcrumb('admin/reports', $trail, t('Reports')); $this->assertNoResponse(403); + // Since the Reports page is accessible, that will show. + $trail += array('admin/reports' => t('Reports')); $this->assertBreadcrumb('admin/reports/dblog', $trail, t('Recent log messages')); $this->assertNoResponse(403); } diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTestBase.php index 7d23727..d205b13 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTestBase.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTestBase.php @@ -59,8 +59,9 @@ protected function assertBreadcrumbParts($trail) { // Compare paths with actual breadcrumb. $parts = $this->getBreadcrumbParts(); $pass = TRUE; - // There may be more than one breadcrumb on the page. - while (!empty($parts)) { + // There may be more than one breadcrumb on the page. If $trail is empty + // this test would go into an infinite loop, so we need to check that too. + while ($trail && !empty($parts)) { foreach ($trail as $path => $title) { $url = url($path); $part = array_shift($parts); diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTranslateTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTranslateTest.php index 33aa51a..8c27287 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTranslateTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuTranslateTest.php @@ -45,7 +45,11 @@ public function testMenuTranslate() { // Attempt to access a restricted local task. $this->drupalGet('foo/asdf/c'); $this->assertResponse(403); - $this->assertNoLinkByHref('foo/asdf'); + $elements = $this->xpath('//ul[@class=:class]/li/a[@href=:href]', array( + ':class' => 'tabs primary', + ':href' => url('foo/asdf'), + )); + $this->assertTrue(empty($elements), 'No tab linking to foo/asdf found'); $this->assertNoLinkByHref('foo/asdf/b'); $this->assertNoLinkByHref('foo/asdf/c'); } diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/TrailTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TrailTest.php index 111c118..409f3aa 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/TrailTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/TrailTest.php @@ -40,69 +40,6 @@ function setUp() { } /** - * Tests active trails are properly affected by menu_tree_set_path(). - */ - function testMenuTreeSetPath() { - $home = array('' => 'Home'); - $config_tree = array( - 'admin' => t('Administration'), - 'admin/config' => t('Configuration'), - ); - $config = $home + $config_tree; - - // The menu_test_menu_tree_set_path system variable controls whether or not - // the menu_test_menu_trail_callback() callback (used by all paths in these - // tests) issues an overriding call to menu_trail_set_path(). - $test_menu_path = array( - 'menu_name' => 'admin', - 'path' => 'admin/config/system/site-information', - ); - - $breadcrumb = $home + array( - 'menu-test' => t('Menu test root'), - ); - $tree = array( - 'menu-test' => t('Menu test root'), - 'menu-test/menu-trail' => t('Menu trail - Case 1'), - ); - - // Test the tree generation for the Tools menu. - \Drupal::state()->delete('menu_test.menu_tree_set_path'); - $this->assertBreadcrumb('menu-test/menu-trail', $breadcrumb, t('Menu trail - Case 1'), $tree); - - // Override the active trail for the Administration tree; it should not - // affect the Tools tree. - \Drupal::state()->set('menu_test.menu_tree_set_path', $test_menu_path); - $this->assertBreadcrumb('menu-test/menu-trail', $breadcrumb, t('Menu trail - Case 1'), $tree); - - $breadcrumb = $config + array( - 'admin/config/development' => t('Development'), - ); - $tree = $config_tree + array( - 'admin/config/development' => t('Development'), - 'admin/config/development/menu-trail' => t('Menu trail - Case 2'), - ); - - $override_breadcrumb = $config + array( - 'admin/config/system' => t('System'), - 'admin/config/system/site-information' => t('Site information'), - ); - $override_tree = $config_tree + array( - 'admin/config/system' => t('System'), - 'admin/config/system/site-information' => t('Site information'), - ); - - // Test the tree generation for the Administration menu. - \Drupal::state()->delete('menu_test.menu_tree_set_path'); - $this->assertBreadcrumb('admin/config/development/menu-trail', $breadcrumb, t('Menu trail - Case 2'), $tree); - - // Override the active trail for the Administration tree; it should affect - // the breadcrumbs and Administration tree. - \Drupal::state()->set('menu_test.menu_tree_set_path', $test_menu_path); - $this->assertBreadcrumb('admin/config/development/menu-trail', $override_breadcrumb, t('Menu trail - Case 2'), $override_tree); - } - - /** * Tests that the active trail works correctly on custom 403 and 404 pages. */ function testCustom403And404Pages() { diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeAccessTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeAccessTest.php index 263e2fc..df10877 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeAccessTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeAccessTest.php @@ -33,6 +33,13 @@ class TreeAccessTest extends DrupalUnitTestBase { */ protected $routeCollection; + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('menu_link'); + public static function getInfo() { return array( 'name' => 'Menu tree access', diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 150a42c..0bde16e 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -941,53 +941,6 @@ function hook_local_task_alter(&$local_tasks) { } /** - * Alter links in the active trail before it is rendered as the breadcrumb. - * - * This hook is invoked by menu_get_active_breadcrumb() and allows alteration - * of 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 @@ -1399,6 +1352,29 @@ function hook_module_implements_alter(&$implementations, $hook) { } /** + * Perform alterations to the breadcrumb built by the BreadcrumbManager. + * + * @param array $breadcrumb + * An array of breadcrumb link a tags, returned by the breadcrumb manager + * build method, for example + * @code + * array('Home'); + * @endcode + * @param array $attributes + * Attributes representing the current page, coming from + * \Drupal::request()->attributes. + * @param array $context + * May include the following key: + * - builder: the instance of + * \Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface that constructed this + * breadcrumb, or NULL if no builder acted based on the current attributes. + */ +function hook_system_breadcrumb_alter(array &$breadcrumb, array $attributes, array $context) { + // Add an item to the end of the breadcrumb. + $breadcrumb[] = Drupal::l(t('Text'), 'example_route_name'); +} + +/** * Return additional themes provided by modules. * * Only use this hook for testing purposes. Use a hidden MYMODULE_test.module diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 7b284ae..508d59c 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -9,6 +9,7 @@ system.admin: path: '/admin' defaults: _content: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' + _title: 'Administration' requirements: _permission: 'access administration pages' @@ -16,6 +17,7 @@ system.admin_structure: path: '/admin/structure' defaults: _content: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' + _title: 'Structure' requirements: _permission: 'access administration pages' @@ -23,6 +25,7 @@ system.admin_reports: path: '/admin/reports' defaults: _content: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' + _title: 'Reports' requirements: _permission: 'access site reports' @@ -30,6 +33,7 @@ system.admin_config_media: path: '/admin/config/media' defaults: _content: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' + _title: 'Media' requirements: _permission: 'access administration pages' @@ -37,6 +41,7 @@ system.admin_config_services: path: '/admin/config/services' defaults: _content: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' + _title: 'Web services' requirements: _permission: 'access administration pages' @@ -44,6 +49,7 @@ system.admin_config_development: path: '/admin/config/development' defaults: _content: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' + _title: 'Development' requirements: _permission: 'access administration pages' @@ -51,6 +57,7 @@ system.admin_config_regional: path: '/admin/config/regional' defaults: _content: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' + _title: 'Regional and language' requirements: _permission: 'access administration pages' @@ -58,6 +65,7 @@ system.admin_config_search: path: '/admin/config/search' defaults: _content: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' + _title: 'Search and metadata' requirements: _permission: 'access administration pages' @@ -65,6 +73,7 @@ system.admin_config_system: path: '/admin/config/system' defaults: _content: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' + _title: 'System' requirements: _permission: 'access administration pages' @@ -72,6 +81,7 @@ system.admin_config_ui: path: '/admin/config/user-interface' defaults: _content: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' + _title: 'User interface' requirements: _permission: 'access administration pages' @@ -79,6 +89,7 @@ system.admin_config_workflow: path: '/admin/config/workflow' defaults: _content: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' + _title: 'Workflow' requirements: _permission: 'access administration pages' @@ -86,6 +97,7 @@ system.admin_config_content: path: '/admin/config/content' defaults: _content: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' + _title: 'Content authoring' requirements: _permission: 'access administration pages' @@ -342,6 +354,7 @@ system.admin_config: path: '/admin/config' defaults: _content: '\Drupal\system\Controller\SystemController::overview' + _title: 'Configuration' requirements: _permission: 'access administration pages' diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml index f1838b5..ae137da 100644 --- a/core/modules/system/system.services.yml +++ b/core/modules/system/system.services.yml @@ -10,6 +10,11 @@ services: class: Drupal\system\LegacyBreadcrumbBuilder tags: - {name: breadcrumb_builder, priority: 500} + system.breadcrumb.default: + class: Drupal\system\PathBasedBreadcrumbBuilder + arguments: ['@request', '@entity.manager', '@access_manager', '@string_translation', '@router', '@path_processor_manager', '@config.factory', '@link_generator', '@title_resolver'] + tags: + - { name: breadcrumb_builder, priority: 0 } path_processor.files: class: Drupal\system\PathProcessor\PathProcessorFiles tags: diff --git a/core/modules/taxonomy/taxonomy.routing.yml b/core/modules/taxonomy/taxonomy.routing.yml index 8e024a0..43a0c2a 100644 --- a/core/modules/taxonomy/taxonomy.routing.yml +++ b/core/modules/taxonomy/taxonomy.routing.yml @@ -2,6 +2,7 @@ taxonomy.vocabulary_list: path: '/admin/structure/taxonomy' defaults: _entity_list: 'taxonomy_vocabulary' + _title: 'Taxonomy' requirements: _permission: 'administer taxonomy'