diff --git a/core/modules/system/lib/Drupal/system/PathBasedBreadcrumbBuilder.php b/core/modules/system/lib/Drupal/system/PathBasedBreadcrumbBuilder.php index 091ec9c..99e1b05 100644 --- a/core/modules/system/lib/Drupal/system/PathBasedBreadcrumbBuilder.php +++ b/core/modules/system/lib/Drupal/system/PathBasedBreadcrumbBuilder.php @@ -18,6 +18,7 @@ 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; /** @@ -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->getCurrentUser(), $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])) { @@ -198,6 +197,19 @@ protected function getRequestForPath($path, array $exclude) { catch (ResourceNotFoundException $e) { return NULL; } + catch (MethodNotAllowedException $e) { + return NULL; + } + } + + /** + * Gets the current user. + * + * @return \Drupal\Core\Session\AccountInterface + * The current user object. + */ + protected function getCurrentUser() { + return \Drupal::currentUser(); } } diff --git a/core/modules/system/tests/Drupal/system/Tests/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php b/core/modules/system/tests/Drupal/system/Tests/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php new file mode 100644 index 0000000..d2c4771 --- /dev/null +++ b/core/modules/system/tests/Drupal/system/Tests/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php @@ -0,0 +1,442 @@ + '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->builder = new TestPathBasedBreadcrumbBuilder( + $this->request, + $this->getMock('\Drupal\Core\Entity\EntityManagerInterface'), + $this->accessManager, + $this->requestMatcher, + $this->pathProcessor, + $config_factory, + $this->titleResolver + ); + + $this->builder->setTranslationManager($this->getStringTranslationStub()); + + $this->linkGenerator = $this->getMock('Drupal\Core\Utility\LinkGeneratorInterface'); + $this->builder->setLinkGenerator($this->linkGenerator); + + $this->currentUser = $this->getMock('Drupal\Core\Session\AccountInterface'); + $this->builder->setCurrentUser($this->currentUser); + } + + /** + * 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 => 'Home'), $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 = 'Example'; + $link_front = 'Home'; + $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', '', array(), array()) + ->will($this->returnValue($link_front)); + $this->setupAccessManagerWithTrue(); + + $links = $this->builder->build(array()); + $this->assertEquals(array(0 => 'Home', 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 = 'Bar'; + $link_example = 'Example'; + $link_front = 'Home'; + $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', '', array(), array()) + ->will($this->returnValue($link_front)); + $this->setupAccessManagerWithTrue(); + + $links = $this->builder->build(array()); + $this->assertEquals(array(0 => 'Home', 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 => 'Home'), $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 => 'Home'), $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 = 'Admin'; + $link_front = 'Home'; + $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', '', 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 => 'Home', 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(), '', array()) + ->will($this->returnCallback(function($title) { + return '' . $title . ''; + })); + } + + /** + * 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 { + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + public function setTranslationManager(TranslationInterface $translation_manager) { + $this->translationManager = $translation_manager; + } + + public function setLinkGenerator(LinkGeneratorInterface $link_generator) { + $this->linkGenerator = $link_generator; + } + + public function setCurrentUser(AccountInterface $current_user) { + $this->currentUser = $current_user; + } + + protected function getCurrentUser() { + return $this->currentUser; + } + +}