Commit 71142958 authored by effulgentsia's avatar effulgentsia

Issue #2600666 by tim.plunkett: Route definitions using entity type...

Issue #2600666 by tim.plunkett: Route definitions using entity type paramconverters with serial IDs should add an integer requirement
parent 51ad3c22
......@@ -7,7 +7,11 @@
namespace Drupal\Core\Entity\Routing;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
......@@ -24,7 +28,33 @@
*
* @internal
*/
class DefaultHtmlRouteProvider implements EntityRouteProviderInterface {
class DefaultHtmlRouteProvider implements EntityRouteProviderInterface, EntityHandlerInterface {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a new DefaultHtmlRouteProvider.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(EntityManagerInterface $entity_manager) {
$this->entityManager = $entity_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$container->get('entity.manager')
);
}
/**
* {@inheritdoc}
......@@ -71,6 +101,12 @@ protected function getCanonicalRoute(EntityTypeInterface $entity_type) {
->setOption('parameters', [
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
]);
// Entity types with serial IDs can specify this in their route
// requirements, improving the matching process.
if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
$route->setRequirement($entity_type_id, '\d+');
}
return $route;
}
}
......@@ -102,6 +138,12 @@ protected function getEditFormRoute(EntityTypeInterface $entity_type) {
->setOption('parameters', [
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
]);
// Entity types with serial IDs can specify this in their route
// requirements, improving the matching process.
if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
$route->setRequirement($entity_type_id, '\d+');
}
return $route;
}
}
......@@ -128,8 +170,33 @@ protected function getDeleteFormRoute(EntityTypeInterface $entity_type) {
->setOption('parameters', [
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
]);
// Entity types with serial IDs can specify this in their route
// requirements, improving the matching process.
if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
$route->setRequirement($entity_type_id, '\d+');
}
return $route;
}
}
/**
* Gets the type of the ID key for a given entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* An entity type.
*
* @return string|null
* The type of the ID key for a given entity type, or NULL if the entity
* type does not support fields.
*/
protected function getEntityTypeIdKeyType(EntityTypeInterface $entity_type) {
if (!$entity_type->isSubclassOf(FieldableEntityInterface::class)) {
return NULL;
}
$field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type->id());
return $field_storage_definitions[$entity_type->getKey('id')]->getType();
}
}
......@@ -44,6 +44,7 @@ entity.block_content.canonical:
_admin_route: TRUE
requirements:
_entity_access: 'block_content.update'
block_content: \d+
entity.block_content.edit_form:
path: '/block/{block_content}'
......@@ -53,6 +54,7 @@ entity.block_content.edit_form:
_admin_route: TRUE
requirements:
_entity_access: 'block_content.update'
block_content: \d+
entity.block_content.delete_form:
path: '/block/{block_content}/delete'
......@@ -63,6 +65,7 @@ entity.block_content.delete_form:
_admin_route: TRUE
requirements:
_entity_access: 'block_content.delete'
block_content: \d+
block_content.type_add:
path: '/admin/structure/block/block-content/types/add'
......
......@@ -29,6 +29,7 @@ book.export:
requirements:
_permission: 'access printer-friendly version'
_entity_access: 'node.view'
node: \d+
entity.node.book_outline_form:
path: '/node/{node}/outline'
......@@ -38,6 +39,7 @@ entity.node.book_outline_form:
requirements:
_permission: 'administer book outlines'
_entity_access: 'node.view'
node: \d+
options:
_node_operation_route: TRUE
......@@ -62,3 +64,4 @@ entity.node.book_remove_form:
_permission: 'administer book outlines'
_entity_access: 'node.view'
_access_book_removable: 'TRUE'
node: \d+
......@@ -23,6 +23,7 @@ entity.comment.edit_form:
_entity_form: 'comment.default'
requirements:
_entity_access: 'comment.update'
comment: \d+
comment.approve:
path: '/comment/{comment}/approve'
......@@ -33,6 +34,7 @@ comment.approve:
requirements:
_entity_access: 'comment.approve'
_csrf_token: 'TRUE'
comment: \d+
entity.comment.canonical:
path: '/comment/{comment}'
......@@ -41,6 +43,7 @@ entity.comment.canonical:
_controller: '\Drupal\comment\Controller\CommentController::commentPermalink'
requirements:
_entity_access: 'comment.view'
comment: \d+
entity.comment.delete_form:
path: '/comment/{comment}/delete'
......@@ -49,6 +52,7 @@ entity.comment.delete_form:
_entity_form: 'comment.delete'
requirements:
_entity_access: 'comment.delete'
comment: \d+
comment.reply:
path: '/comment/reply/{entity_type}/{entity}/{field_name}/{pid}'
......@@ -77,6 +81,7 @@ comment.node_redirect:
requirements:
_entity_access: 'node.view'
_module_dependencies: 'node'
node: \d+
entity.comment_type.collection:
path: '/admin/structure/comment'
......
......@@ -54,3 +54,4 @@ entity.user.contact_form:
_controller: '\Drupal\contact\Controller\ContactController::contactPersonalPage'
requirements:
_access_contact_personal_tab: 'TRUE'
user: \d+
......@@ -11,3 +11,4 @@ history.read_node:
_controller: '\Drupal\history\Controller\HistoryController::readNode'
requirements:
_entity_access: 'node.view'
node: \d+
......@@ -7,6 +7,8 @@
namespace Drupal\link\Plugin\Validation\Constraint;
use Symfony\Component\Routing\Exception\InvalidParameterException;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidatorInterface;
......@@ -50,9 +52,17 @@ public function validate($value, Constraint $constraint) {
try {
$url->toString();
}
// The following exceptions are all possible during URL generation, and
// should be considered as disallowed URLs.
catch (RouteNotFoundException $e) {
$allowed = FALSE;
}
catch (InvalidParameterException $e) {
$allowed = FALSE;
}
catch (MissingMandatoryParametersException $e) {
$allowed = FALSE;
}
if (!$allowed) {
$this->context->addViolation($constraint->message, array('@uri' => $value->uri));
}
......
......@@ -129,8 +129,6 @@ function testURLValidation() {
'entity:user/1' => '- Restricted access - (1)',
// URI for an entity that doesn't exist, but with a valid ID.
'entity:user/999999' => 'entity:user/999999',
// URI for an entity that doesn't exist, with an invalid ID.
'entity:user/invalid-parameter' => 'entity:user/invalid-parameter',
);
// Define some invalid URLs.
......@@ -146,6 +144,8 @@ function testURLValidation() {
$invalid_internal_entries = array(
'no-leading-slash' => $validation_error_2,
'entity:non_existing_entity_type/yar' => $validation_error_1,
// URI for an entity that doesn't exist, with an invalid ID.
'entity:user/invalid-parameter' => $validation_error_1,
);
// Test external and internal URLs for 'link_type' = LinkItemInterface::LINK_GENERIC.
......
......@@ -47,6 +47,7 @@ entity.node.version_history:
_controller: '\Drupal\node\Controller\NodeController::revisionOverview'
requirements:
_access_node_revision: 'view'
node: \d+
options:
_node_operation_route: TRUE
......@@ -57,6 +58,7 @@ entity.node.revision:
_title_callback: '\Drupal\node\Controller\NodeController::revisionPageTitle'
requirements:
_access_node_revision: 'view'
node: \d+
node.revision_revert_confirm:
path: '/node/{node}/revisions/{node_revision}/revert'
......@@ -65,6 +67,7 @@ node.revision_revert_confirm:
_title: 'Revert to earlier revision'
requirements:
_access_node_revision: 'update'
node: \d+
options:
_node_operation_route: TRUE
......@@ -75,6 +78,7 @@ node.revision_revert_translation_confirm:
_title: 'Revert to earlier revision of a translation'
requirements:
_access_node_revision: 'update'
node: \d+
options:
_node_operation_route: TRUE
......@@ -85,6 +89,7 @@ node.revision_delete_confirm:
_title: 'Delete earlier revision'
requirements:
_access_node_revision: 'delete'
node: \d+
options:
_node_operation_route: TRUE
......
......@@ -27,6 +27,7 @@ public function getRoutes( EntityTypeInterface $entity_type) {
'_controller' => '\Drupal\node\Controller\NodeViewController::view',
'_title_callback' => '\Drupal\node\Controller\NodeViewController::title',
])
->setRequirement('node', '\d+')
->setRequirement('_entity_access', 'node.view');
$route_collection->add('entity.node.canonical', $route);
......@@ -35,6 +36,7 @@ public function getRoutes( EntityTypeInterface $entity_type) {
'_entity_form' => 'node.delete',
'_title' => 'Delete',
])
->setRequirement('node', '\d+')
->setRequirement('_entity_access', 'node.delete')
->setOption('_node_operation_route', TRUE);
$route_collection->add('entity.node.delete_form', $route);
......@@ -42,6 +44,7 @@ public function getRoutes( EntityTypeInterface $entity_type) {
$route = (new Route('/node/{node}/edit'))
->setDefault('_entity_form', 'node.edit')
->setRequirement('_entity_access', 'node.update')
->setRequirement('node', '\d+')
->setOption('_node_operation_route', TRUE);
$route_collection->add('entity.node.edit_form', $route);
......
......@@ -61,6 +61,7 @@ entity.shortcut.canonical:
_title: 'Edit'
requirements:
_entity_access: 'shortcut.update'
shortcut: \d+
entity.shortcut.edit_form:
path: '/admin/config/user-interface/shortcut/link/{shortcut}'
......@@ -69,6 +70,7 @@ entity.shortcut.edit_form:
_title: 'Edit'
requirements:
_entity_access: 'shortcut.update'
shortcut: \d+
entity.shortcut.link_delete_inline:
path: '/admin/config/user-interface/shortcut/link/{shortcut}/delete-inline'
......@@ -77,6 +79,7 @@ entity.shortcut.link_delete_inline:
requirements:
_entity_access: 'shortcut.delete'
_csrf_token: 'TRUE'
shortcut: \d+
entity.shortcut.delete_form:
path: '/admin/config/user-interface/shortcut/link/{shortcut}/delete'
......@@ -85,6 +88,7 @@ entity.shortcut.delete_form:
_title: 'Delete'
requirements:
_entity_access: 'shortcut.delete'
shortcut: \d+
shortcut.set_switch:
path: '/user/{user}/shortcuts'
......@@ -95,3 +99,4 @@ shortcut.set_switch:
_custom_access: 'Drupal\shortcut\Form\SwitchShortcutSet::checkAccess'
options:
_admin_route: TRUE
user: \d+
......@@ -23,6 +23,7 @@ entity.taxonomy_term.edit_form:
_admin_route: TRUE
requirements:
_entity_access: 'taxonomy_term.update'
taxonomy_term: \d+
entity.taxonomy_term.delete_form:
path: '/taxonomy/term/{taxonomy_term}/delete'
......@@ -33,6 +34,7 @@ entity.taxonomy_term.delete_form:
_admin_route: TRUE
requirements:
_entity_access: 'taxonomy_term.delete'
taxonomy_term: \d+
entity.taxonomy_vocabulary.add_form:
path: '/admin/structure/taxonomy/add'
......@@ -82,3 +84,4 @@ entity.taxonomy_term.canonical:
_title_callback: '\Drupal\taxonomy\Controller\TaxonomyController::termTitle'
requirements:
_entity_access: 'taxonomy_term.view'
taxonomy_term: \d+
......@@ -14,6 +14,7 @@ tracker.users_recent_content:
requirements:
_permission: 'access content'
_access_tracker_own_information: 'TRUE'
user: \d+
tracker.user_tab:
path: '/user/{user}/activity'
......@@ -23,4 +24,5 @@ tracker.user_tab:
requirements:
_permission: 'access content'
_entity_access: 'user.view'
user: \d+
......@@ -27,6 +27,7 @@ public function getRoutes(EntityTypeInterface $entity_type) {
'_entity_view' => 'user.full',
'_title_callback' => 'Drupal\user\Controller\UserController::userTitle',
])
->setRequirement('user', '\d+')
->setRequirement('_entity_access', 'user.view');
$route_collection->add('entity.user.canonical', $route);
......@@ -36,6 +37,7 @@ public function getRoutes(EntityTypeInterface $entity_type) {
'_title_callback' => 'Drupal\user\Controller\UserController::userTitle',
])
->setOption('_admin_route', TRUE)
->setRequirement('user', '\d+')
->setRequirement('_entity_access', 'user.update');
$route_collection->add('entity.user.edit_form', $route);
......@@ -45,6 +47,7 @@ public function getRoutes(EntityTypeInterface $entity_type) {
'_entity_form' => 'user.cancel',
])
->setOption('_admin_route', TRUE)
->setRequirement('user', '\d+')
->setRequirement('_entity_access', 'user.delete');
$route_collection->add('entity.user.cancel_form', $route);
......
......@@ -138,6 +138,7 @@ user.cancel_confirm:
hashed_pass: ''
requirements:
_entity_access: 'user.delete'
user: \d+
user.reset:
path: '/user/reset/{uid}/{timestamp}/{hash}'
......
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Entity\Routing\DefaultHtmlRouteProviderTest.
*/
namespace Drupal\Tests\Core\Entity\Routing;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Symfony\Component\Routing\Route;
/**
* @coversDefaultClass \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider
* @group Entity
*/
class DefaultHtmlRouteProviderTest extends UnitTestCase {
/**
* @covers ::getEntityTypeIdKeyType
*/
public function testGetEntityTypeIdKeyType() {
$entity_manager = $this->prophesize(EntityManagerInterface::class);
$route_provider = new TestDefaultHtmlRouteProvider($entity_manager->reveal());
$entity_type = $this->prophesize(EntityTypeInterface::class);
$entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE);
$entity_type_id = 'the_entity_type_id';
$entity_type->id()->willReturn($entity_type_id);
$entity_type->getKey('id')->willReturn('id');
$field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class);
$field_storage_definition->getType()->willReturn('integer');
$entity_manager->getFieldStorageDefinitions($entity_type_id)->willReturn(['id' => $field_storage_definition]);
$type = $route_provider->getEntityTypeIdKeyType($entity_type->reveal());
$this->assertSame('integer', $type);
}
/**
* @covers ::getEntityTypeIdKeyType
*/
public function testGetEntityTypeIdKeyTypeNotFieldable() {
$entity_manager = $this->prophesize(EntityManagerInterface::class);
$route_provider = new TestDefaultHtmlRouteProvider($entity_manager->reveal());
$entity_type = $this->prophesize(EntityTypeInterface::class);
$entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE);
$entity_manager->getFieldStorageDefinitions(Argument::any())->shouldNotBeCalled();
$type = $route_provider->getEntityTypeIdKeyType($entity_type->reveal());
$this->assertNull($type);
}
/**
* @covers ::getCanonicalRoute
* @dataProvider providerTestGetCanonicalRoute
*/
public function testGetCanonicalRoute($entity_type_prophecy, $expected, $field_storage_definition = NULL) {
$entity_manager = $this->prophesize(EntityManagerInterface::class);
$route_provider = new TestDefaultHtmlRouteProvider($entity_manager->reveal());
$entity_type = $entity_type_prophecy->reveal();
if ($field_storage_definition) {
$entity_manager->getFieldStorageDefinitions($entity_type->id())
->willReturn([$entity_type->getKey('id') => $field_storage_definition]);
}
$route = $route_provider->getCanonicalRoute($entity_type);
$this->assertEquals($expected, $route);
}
public function providerTestGetCanonicalRoute() {
$data = [];
$entity_type1 = $this->prophesize(EntityTypeInterface::class);
$entity_type1->hasLinkTemplate('canonical')->willReturn(FALSE);
$data['no_canonical_link_template'] = [$entity_type1, NULL];
$entity_type2 = $this->prophesize(EntityTypeInterface::class);
$entity_type2->hasLinkTemplate('canonical')->willReturn(TRUE);
$entity_type2->hasViewBuilderClass()->willReturn(FALSE);
$data['no_view_builder'] = [$entity_type2, NULL];
$entity_type3 = $this->prophesize(EntityTypeInterface::class);
$entity_type3->hasLinkTemplate('canonical')->willReturn(TRUE);
$entity_type3->hasViewBuilderClass()->willReturn(TRUE);
$entity_type3->id()->willReturn('the_entity_type_id');
$entity_type3->getLinkTemplate('canonical')->willReturn('/the/canonical/link/template');
$entity_type3->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE);
$route3 = (new Route('/the/canonical/link/template'))
->setDefaults([
'_entity_view' => 'the_entity_type_id.full',
'_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::title',
])
->setRequirements([
'_entity_access' => 'the_entity_type_id.view',
])
->setOptions([
'parameters' => [
'the_entity_type_id' => [
'type' => 'entity:the_entity_type_id',
],
],
]);
$data['id_key_type_null'] = [$entity_type3, $route3];
$entity_type4 = $this->prophesize(EntityTypeInterface::class);
$entity_type4->hasLinkTemplate('canonical')->willReturn(TRUE);
$entity_type4->hasViewBuilderClass()->willReturn(TRUE);
$entity_type4->id()->willReturn('the_entity_type_id');
$entity_type4->getLinkTemplate('canonical')->willReturn('/the/canonical/link/template');
$entity_type4->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE);
$entity_type4->getKey('id')->willReturn('id');
$route4 = (new Route('/the/canonical/link/template'))
->setDefaults([
'_entity_view' => 'the_entity_type_id.full',
'_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::title',
])
->setRequirements([
'_entity_access' => 'the_entity_type_id.view',
'the_entity_type_id' => '\d+',
])
->setOptions([
'parameters' => [
'the_entity_type_id' => [
'type' => 'entity:the_entity_type_id',
],
],
]);
$field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class);
$field_storage_definition->getType()->willReturn('integer');
$data['id_key_type_integer'] = [$entity_type4, $route4, $field_storage_definition];
return $data;
}
}
class TestDefaultHtmlRouteProvider extends DefaultHtmlRouteProvider {
public function getEntityTypeIdKeyType(EntityTypeInterface $entity_type) {
return parent::getEntityTypeIdKeyType($entity_type);
}
public function getCanonicalRoute(EntityTypeInterface $entity_type) {
return parent::getCanonicalRoute($entity_type);
}
}
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