Commit c0b1b698 authored by catch's avatar catch
Browse files

Issue #2180425 by dawehner, klausi: PathBasedBreadcrumbBuilder should catch...

Issue #2180425 by dawehner, klausi: PathBasedBreadcrumbBuilder should catch all exceptions from routing.
parent 2cf87103
......@@ -11,13 +11,14 @@
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Access\AccessManager;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Component\Utility\Unicode;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/**
......@@ -39,13 +40,6 @@ class PathBasedBreadcrumbBuilder extends BreadcrumbBuilderBase {
*/
protected $accessManager;
/**
* The menu storage controller.
*
* @var \Drupal\Core\Config\Entity\ConfigStorageControllerInterface
*/
protected $menuStorage;
/**
* The dynamic router service.
*
......@@ -74,13 +68,18 @@ class PathBasedBreadcrumbBuilder extends BreadcrumbBuilderBase {
*/
protected $titleResolver;
/**
* The current user object.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs the PathBasedBreadcrumbBuilder.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current Request object.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Access\AccessManager $access_manager
* The menu link access service.
* @param \Symfony\Component\Routing\Matcher\RequestMatcherInterface $router
......@@ -91,15 +90,17 @@ class PathBasedBreadcrumbBuilder extends BreadcrumbBuilderBase {
* The config factory service.
* @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
* The title resolver service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user object.
*/
public function __construct(Request $request, EntityManagerInterface $entity_manager, AccessManager $access_manager, RequestMatcherInterface $router, InboundPathProcessorInterface $path_processor, ConfigFactoryInterface $config_factory, TitleResolverInterface $title_resolver) {
public function __construct(Request $request, AccessManager $access_manager, RequestMatcherInterface $router, InboundPathProcessorInterface $path_processor, ConfigFactoryInterface $config_factory, TitleResolverInterface $title_resolver, AccountInterface $current_user) {
$this->request = $request;
$this->accessManager = $access_manager;
$this->menuStorage = $entity_manager->getStorageController('menu');
$this->router = $router;
$this->pathProcessor = $path_processor;
$this->config = $config_factory->get('system.site');
$this->titleResolver = $title_resolver;
$this->currentUser = $current_user;
}
/**
......@@ -132,25 +133,23 @@ public function build(array $attributes) {
// 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, \Drupal::currentUser(), $route_request);
if ($access) {
$title = $this->titleResolver->getTitle($route_request, $route_request->attributes->get(RouteObjectInterface::ROUTE_OBJECT));
}
$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, $this->currentUser, $route_request);
if ($access) {
$title = $this->titleResolver->getTitle($route_request, $route_request->attributes->get(RouteObjectInterface::ROUTE_OBJECT));
}
if ($access) {
if (!$title) {
if (!isset($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'));
$links[] = $this->l($title, $route_request->attributes->get(RouteObjectInterface::ROUTE_NAME), $route_request->attributes->get('_raw_variables')->all());
}
}
......@@ -171,7 +170,7 @@ public function build(array $attributes) {
* 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.
* A populated request object or NULL if the path couldn't be matched.
*/
protected function getRequestForPath($path, array $exclude) {
if (!empty($exclude[$path])) {
......@@ -201,6 +200,9 @@ protected function getRequestForPath($path, array $exclude) {
catch (ResourceNotFoundException $e) {
return NULL;
}
catch (MethodNotAllowedException $e) {
return NULL;
}
}
}
......@@ -10,7 +10,7 @@ services:
- [setRequest, ['@?request=']]
system.breadcrumb.default:
class: Drupal\system\PathBasedBreadcrumbBuilder
arguments: ['@request', '@entity.manager', '@access_manager', '@router', '@path_processor_manager', '@config.factory', '@title_resolver']
arguments: ['@request', '@access_manager', '@router', '@path_processor_manager', '@config.factory', '@title_resolver', '@current_user']
tags:
- { name: breadcrumb_builder, priority: 0 }
path_processor.files:
......
<?php
/**
* @file
* Contains \Drupal\system\Tests\Breadcrumbs\PathBasedBreadcrumbBuilderTest
*/
namespace Drupal\system\Tests\Breadcrumbs;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Utility\LinkGeneratorInterface;
use Drupal\system\PathBasedBreadcrumbBuilder;
use Drupal\Tests\UnitTestCase;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Tests the path based breadcrumb builder.
*
* @group Drupal
* @group System
*
* @coversDefaultClass \Drupal\system\PathBasedBreadcrumbBuilder
*/
class PathBasedBreadcrumbBuilderTest extends UnitTestCase {
/**
* The path based breadcrumb builder object to test.
*
* @var \Drupal\system\PathBasedBreadcrumbBuilder
*/
protected $builder;
/**
* The mocked title resolver.
*
* @var \Drupal\Core\Controller\TitleResolverInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $titleResolver;
/**
* The mocked access manager.
*
* @var \Drupal\Core\Access\AccessManager|\PHPUnit_Framework_MockObject_MockObject
*/
protected $accessManager;
/**
* The request matching mock object.
*
* @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $requestMatcher;
/**
* The mocked request object.
*
* @var \Symfony\Component\HttpFoundation\Request|\PHPUnit_Framework_MockObject_MockObject
*/
protected $request;
/**
* The mocked link generator.
*
* @var \Drupal\Core\Utility\LinkGeneratorInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $linkGenerator;
/**
* The mocked current user.
*
* @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $currentUser;
/**
* The mocked path processor.
*
* @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $pathProcessor;
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'Path based breadcrumbs',
'description' => 'Tests that path based breadcrumbs work as expected.',
'group' => 'Breadcrumbs',
);
}
/**
* {@inheritdoc}
*
* @covers ::__construct()
*/
public function setUp() {
parent::setUp();
$this->requestMatcher = $this->getMock('\Symfony\Component\Routing\Matcher\RequestMatcherInterface');
$config_factory = $this->getConfigFactoryStub(array('system.site' => array('front' => 'test_frontpage')));
$this->pathProcessor = $this->getMock('\Drupal\Core\PathProcessor\InboundPathProcessorInterface');
$this->request = $this->getMockBuilder('\Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->getMock();
$this->accessManager = $this->getMockBuilder('\Drupal\Core\Access\AccessManager')
->disableOriginalConstructor()->getMock();
$this->titleResolver = $this->getMock('\Drupal\Core\Controller\TitleResolverInterface');
$this->currentUser = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->builder = new TestPathBasedBreadcrumbBuilder(
$this->request,
$this->accessManager,
$this->requestMatcher,
$this->pathProcessor,
$config_factory,
$this->titleResolver,
$this->currentUser
);
$this->builder->setTranslationManager($this->getStringTranslationStub());
$this->linkGenerator = $this->getMock('Drupal\Core\Utility\LinkGeneratorInterface');
$this->builder->setLinkGenerator($this->linkGenerator);
}
/**
* Tests the build method on the frontpage.
*
* @covers ::build()
*/
public function testBuildOnFrontpage() {
$this->request->expects($this->once())
->method('getPathInfo')
->will($this->returnValue('/'));
$links = $this->builder->build(array());
$this->assertEquals(array(), $links);
}
/**
* Tests the build method with one path element.
*
* @covers ::build()
*/
public function testBuildWithOnePathElement() {
$this->request->expects($this->once())
->method('getPathInfo')
->will($this->returnValue('/example'));
$this->setupLinkGeneratorWithFrontpage();
$links = $this->builder->build(array());
$this->assertEquals(array(0 => '<a href="/">Home</a>'), $links);
}
/**
* Tests the build method with two path elements.
*
* @covers ::build()
* @covers ::getRequestForPath()
*/
public function testBuildWithTwoPathElements() {
$this->request->expects($this->once())
->method('getPathInfo')
->will($this->returnValue('/example/baz'));
$this->setupStubPathProcessor();
$route_1 = new Route('/example');
$this->requestMatcher->expects($this->exactly(1))
->method('matchRequest')
->will($this->returnCallback(function(Request $request) use ($route_1) {
if ($request->getPathInfo() == '/example') {
return array(
RouteObjectInterface::ROUTE_NAME => 'example',
RouteObjectInterface::ROUTE_OBJECT => $route_1,
'_raw_variables' => new ParameterBag(array()),
);
}
}));
$link_example = '<a href="/example">Example</a>';
$link_front = '<a href="/">Home</a>';
$this->linkGenerator->expects($this->at(0))
->method('generate')
->with('Example', 'example', array(), array())
->will($this->returnValue($link_example));
$this->linkGenerator->expects($this->at(1))
->method('generate')
->with('Home', '<front>', array(), array())
->will($this->returnValue($link_front));
$this->setupAccessManagerWithTrue();
$links = $this->builder->build(array());
$this->assertEquals(array(0 => '<a href="/">Home</a>', 1 => $link_example), $links);
}
/**
* Tests the build method with three path elements.
*
* @covers ::build()
* @covers ::getRequestForPath()
*/
public function testBuildWithThreePathElements() {
$this->request->expects($this->once())
->method('getPathInfo')
->will($this->returnValue('/example/bar/baz'));
$this->setupStubPathProcessor();
$route_1 = new Route('/example/bar');
$route_2 = new Route('/example');
$this->requestMatcher->expects($this->exactly(2))
->method('matchRequest')
->will($this->returnCallback(function(Request $request) use ($route_1, $route_2) {
if ($request->getPathInfo() == '/example/bar') {
return array(
RouteObjectInterface::ROUTE_NAME => 'example_bar',
RouteObjectInterface::ROUTE_OBJECT => $route_1,
'_raw_variables' => new ParameterBag(array()),
);
}
elseif ($request->getPathInfo() == '/example') {
return array(
RouteObjectInterface::ROUTE_NAME => 'example',
RouteObjectInterface::ROUTE_OBJECT => $route_2,
'_raw_variables' => new ParameterBag(array()),
);
}
}));
$link_example_bar = '<a href="/example/bar">Bar</a>';
$link_example = '<a href="/example">Example</a>';
$link_front = '<a href="/">Home</a>';
$this->linkGenerator->expects($this->at(0))
->method('generate')
->with('Bar', 'example_bar', array(), array())
->will($this->returnValue($link_example_bar));
$this->linkGenerator->expects($this->at(1))
->method('generate')
->with('Example', 'example', array(), array())
->will($this->returnValue($link_example));
$this->linkGenerator->expects($this->at(2))
->method('generate')
->with('Home', '<front>', array(), array())
->will($this->returnValue($link_front));
$this->setupAccessManagerWithTrue();
$links = $this->builder->build(array());
$this->assertEquals(array(0 => '<a href="/">Home</a>', 1 => $link_example, 2 => $link_example_bar), $links);
}
/**
* Tests that exceptions during request matching are caught.
*
* @covers ::build()
* @covers ::getRequestForPath()
*
* @dataProvider providerTestBuildWithException
*/
public function testBuildWithException($exception_class, $exception_argument) {
$this->request->expects($this->once())
->method('getPathInfo')
->will($this->returnValue('/example/bar'));
$this->setupStubPathProcessor();
$this->requestMatcher->expects($this->any())
->method('matchRequest')
->will($this->throwException(new $exception_class($exception_argument)));
$this->setupLinkGeneratorWithFrontpage();
$links = $this->builder->build(array());
// No path matched, though at least the frontpage is displayed.
$this->assertEquals(array(0 => '<a href="/">Home</a>'), $links);
}
/**
* Provides exception types for testBuildWithException.
*
* @return array
* The list of exception test cases.
*
* @see \Drupal\system\Tests\Breadcrumbs\PathBasedBreadcrumbBuilderTest::testBuildWithException()
*/
public function providerTestBuildWithException() {
return array(
array('Drupal\Core\ParamConverter\ParamNotConvertedException', ''),
array('Symfony\Component\Routing\Exception\MethodNotAllowedException', array()),
array('Symfony\Component\Routing\Exception\ResourceNotFoundException', ''),
);
}
/**
* Tests the build method with a non processed path.
*
* @covers ::build()
* @covers ::getRequestForPath()
*/
public function testBuildWithNonProcessedPath() {
$this->request->expects($this->once())
->method('getPathInfo')
->will($this->returnValue('/example/bar'));
$this->pathProcessor->expects($this->once())
->method('processInbound')
->will($this->returnValue(FALSE));
$this->requestMatcher->expects($this->any())
->method('matchRequest')
->will($this->returnValue(array()));
$this->setupLinkGeneratorWithFrontpage();
$links = $this->builder->build(array());
// No path matched, though at least the frontpage is displayed.
$this->assertEquals(array(0 => '<a href="/">Home</a>'), $links);
}
/**
* Tests the applied method.
*
* @covers ::applies()
*/
public function testApplies() {
$this->assertTrue($this->builder->applies(array()));
}
/**
* Tests the breadcrumb for a user path.
*
* @covers ::build()
* @covers ::getRequestForPath()
*/
public function testBuildWithUserPath() {
$this->request->expects($this->once())
->method('getPathInfo')
->will($this->returnValue('/user/1/edit'));
$this->setupStubPathProcessor();
$route_1 = new Route('/user/1');
$this->requestMatcher->expects($this->exactly(1))
->method('matchRequest')
->will($this->returnCallback(function(Request $request) use ($route_1) {
if ($request->getPathInfo() == '/user/1') {
return array(
RouteObjectInterface::ROUTE_NAME => 'user_page',
RouteObjectInterface::ROUTE_OBJECT => $route_1,
'_raw_variables' => new ParameterBag(array()),
);
}
}));
$link_user = '<a href="/user/1">Admin</a>';
$link_front = '<a href="/">Home</a>';
$this->linkGenerator->expects($this->at(0))
->method('generate')
->with('Admin', 'user_page', array(), array())
->will($this->returnValue($link_user));
$this->linkGenerator->expects($this->at(1))
->method('generate')
->with('Home', '<front>', array(), array())
->will($this->returnValue($link_front));
$this->setupAccessManagerWithTrue();
$this->titleResolver->expects($this->once())
->method('getTitle')
->with($this->anything(), $route_1)
->will($this->returnValue('Admin'));
$links = $this->builder->build(array());
$this->assertEquals(array(0 => '<a href="/">Home</a>', 1 => $link_user), $links);
}
/**
* Setup the link generator with a frontpage route.
*/
public function setupLinkGeneratorWithFrontpage() {
$this->linkGenerator->expects($this->once())
->method('generate')
->with($this->anything(), '<front>', array())
->will($this->returnCallback(function($title) {
return '<a href="/">' . $title . '</a>';
}));
}
/**
* Setup the access manager to always return TRUE.
*/
public function setupAccessManagerWithTrue() {
$this->accessManager->expects($this->any())
->method('checkNamedRoute')
->will($this->returnValue(TRUE));
}
protected function setupStubPathProcessor() {
$this->pathProcessor->expects($this->any())
->method('processInbound')
->will($this->returnArgument(0));
}
}
/**
* Helper class for testing purposes only.
*/
class TestPathBasedBreadcrumbBuilder extends PathBasedBreadcrumbBuilder {
public function setTranslationManager(TranslationInterface $translation_manager) {
$this->translationManager = $translation_manager;
}
public function setLinkGenerator(LinkGeneratorInterface $link_generator) {
$this->linkGenerator = $link_generator;
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment