From 43981094712d4f3e716b919e7b59ae8e46c109c3 Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Mon, 15 May 2017 13:05:40 +0100 Subject: [PATCH] Issue #2293697 by Wim Leers, dawehner, Jo Fitzgerald, clemens.tolboom, vedpareek, tedbow, Munavijayalakshmi, hchonov, alexpott, effulgentsia, tstoeckler, Crell, klausi, EclipseGc: EntityResource POST routes all use the confusing default: use entity types' https://www.drupal.org/link-relations/create link template if available --- core/core.services.yml | 2 +- core/lib/Drupal/Core/Entity/Entity.php | 15 +++- .../DefaultExceptionHtmlSubscriber.php | 12 +++ .../block_content/src/Entity/BlockContent.php | 1 + core/modules/comment/src/Entity/Comment.php | 1 + core/modules/node/src/Entity/Node.php | 1 + core/modules/rest/rest.services.yml | 12 +++ .../EntityResourcePostRouteSubscriber.php | 74 +++++++++++++++++++ .../PathProcessorEntityResourceBC.php | 55 ++++++++++++++ .../rest/src/Plugin/Deriver/EntityDeriver.php | 2 +- core/modules/rest/src/Plugin/ResourceBase.php | 10 ++- .../Plugin/rest/resource/EntityResource.php | 4 +- .../rest/src/Routing/ResourceRoutes.php | 22 ++++-- .../EntityResource/EntityResourceTestBase.php | 29 ++++++-- .../tests/src/Functional/ResourceTestBase.php | 4 + .../src/Controller/Http4xxController.php | 12 +++ core/modules/system/system.routing.yml | 8 ++ .../entity_test/src/Entity/EntityTest.php | 3 + .../src/Entity/EntityTestWithBundle.php | 1 + core/modules/taxonomy/src/Entity/Term.php | 1 + 20 files changed, 249 insertions(+), 20 deletions(-) create mode 100644 core/modules/rest/src/EventSubscriber/EntityResourcePostRouteSubscriber.php create mode 100644 core/modules/rest/src/PathProcessor/PathProcessorEntityResourceBC.php diff --git a/core/core.services.yml b/core/core.services.yml index 30fbcc28e2e..fc0d99f6cd1 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1145,7 +1145,7 @@ services: class: Drupal\Core\Access\CsrfRequestHeaderAccessCheck arguments: ['@session_configuration', '@csrf_token'] tags: - - { name: access_check } + - { name: access_check, needs_incoming_request: TRUE } maintenance_mode: class: Drupal\Core\Site\MaintenanceMode arguments: ['@state', '@current_user'] diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index f36f74fe679..95e85e21a42 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -13,6 +13,7 @@ use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Link; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; +use Symfony\Component\Routing\Exception\RouteNotFoundException; /** * Defines a base entity class. @@ -323,7 +324,19 @@ abstract class Entity implements EntityInterface { * {@inheritdoc} */ public function uriRelationships() { - return array_keys($this->linkTemplates()); + return array_filter(array_keys($this->linkTemplates()), function ($link_relation_type) { + // It's not guaranteed that every link relation type also has a + // corresponding route. For some, additional modules or configuration may + // be necessary. The interface demands that we only return supported URI + // relationships. + try { + $this->toUrl($link_relation_type)->toString(TRUE)->getGeneratedUrl(); + } + catch (RouteNotFoundException $e) { + return FALSE; + } + return TRUE; + }); } /** diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php index 38cb8950d00..f168a02cadd 100644 --- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php @@ -79,6 +79,18 @@ class DefaultExceptionHtmlSubscriber extends HttpExceptionSubscriberBase { return ['html']; } + /** + * Handles a 4xx error for HTML. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on4xx(GetResponseForExceptionEvent $event) { + if (($exception = $event->getException()) && $exception instanceof HttpExceptionInterface) { + $this->makeSubrequest($event, '/system/4xx', $exception->getStatusCode()); + } + } + /** * Handles a 401 error for HTML. * diff --git a/core/modules/block_content/src/Entity/BlockContent.php b/core/modules/block_content/src/Entity/BlockContent.php index b7c3d4dc127..76071df2f47 100644 --- a/core/modules/block_content/src/Entity/BlockContent.php +++ b/core/modules/block_content/src/Entity/BlockContent.php @@ -42,6 +42,7 @@ use Drupal\user\UserInterface; * "delete-form" = "/block/{block_content}/delete", * "edit-form" = "/block/{block_content}", * "collection" = "/admin/structure/block/block-content", + * "create" = "/block", * }, * translatable = TRUE, * entity_keys = { diff --git a/core/modules/comment/src/Entity/Comment.php b/core/modules/comment/src/Entity/Comment.php index a5f19664697..821b30d0bab 100644 --- a/core/modules/comment/src/Entity/Comment.php +++ b/core/modules/comment/src/Entity/Comment.php @@ -57,6 +57,7 @@ use Drupal\user\UserInterface; * "canonical" = "/comment/{comment}", * "delete-form" = "/comment/{comment}/delete", * "edit-form" = "/comment/{comment}/edit", + * "create" = "/comment", * }, * bundle_entity_type = "comment_type", * field_ui_base_route = "entity.comment_type.edit_form", diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php index b66628c2f34..74a7c0bc3e2 100644 --- a/core/modules/node/src/Entity/Node.php +++ b/core/modules/node/src/Entity/Node.php @@ -74,6 +74,7 @@ use Drupal\user\UserInterface; * "edit-form" = "/node/{node}/edit", * "version-history" = "/node/{node}/revisions", * "revision" = "/node/{node}/revisions/{node_revision}/view", + * "create" = "/node", * } * ) */ diff --git a/core/modules/rest/rest.services.yml b/core/modules/rest/rest.services.yml index 869eb196488..080a681afb2 100644 --- a/core/modules/rest/rest.services.yml +++ b/core/modules/rest/rest.services.yml @@ -31,3 +31,15 @@ services: arguments: ['@router.builder'] tags: - { name: event_subscriber } + rest.resource.entity.post_route.subscriber: + class: \Drupal\rest\EventSubscriber\EntityResourcePostRouteSubscriber + arguments: ['@entity_type.manager'] + tags: + - { name: event_subscriber } + + # @todo Remove in Drupal 9.0.0. + rest.path_processor_entity_resource_bc: + class: \Drupal\rest\PathProcessor\PathProcessorEntityResourceBC + arguments: ['@entity_type.manager'] + tags: + - { name: path_processor_inbound } diff --git a/core/modules/rest/src/EventSubscriber/EntityResourcePostRouteSubscriber.php b/core/modules/rest/src/EventSubscriber/EntityResourcePostRouteSubscriber.php new file mode 100644 index 00000000000..36a3f731170 --- /dev/null +++ b/core/modules/rest/src/EventSubscriber/EntityResourcePostRouteSubscriber.php @@ -0,0 +1,74 @@ +resourceConfigStorage = $entity_type_manager->getStorage('rest_resource_config'); + } + + /** + * Provides routes on route rebuild time. + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The route build event. + */ + public function onDynamicRouteEvent(RouteBuildEvent $event) { + $route_collection = $event->getRouteCollection(); + + $resource_configs = $this->resourceConfigStorage->loadMultiple(); + // Iterate over all REST resource config entities. + foreach ($resource_configs as $resource_config) { + // We only care about REST resource config entities for the + // \Drupal\rest\Plugin\rest\resource\EntityResource plugin. + $plugin_id = $resource_config->toArray()['plugin_id']; + if (substr($plugin_id, 0, 6) !== 'entity') { + continue; + } + + $entity_type_id = substr($plugin_id, 7); + $rest_post_route_name = "rest.entity.$entity_type_id.POST"; + if ($rest_post_route = $route_collection->get($rest_post_route_name)) { + // Create a route for the 'create' link relation type for this entity + // type that uses the same route definition as the REST 'POST' route + // which use that entity type. + // @see \Drupal\Core\Entity\Entity::toUrl() + $entity_create_route_name = "entity.$entity_type_id.create"; + $route_collection->add($entity_create_route_name, $rest_post_route); + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // Priority -10, to run after \Drupal\rest\Routing\ResourceRoutes, which has + // priority 0. + $events[RoutingEvents::DYNAMIC][] = ['onDynamicRouteEvent', -10]; + return $events; + } + +} diff --git a/core/modules/rest/src/PathProcessor/PathProcessorEntityResourceBC.php b/core/modules/rest/src/PathProcessor/PathProcessorEntityResourceBC.php new file mode 100644 index 00000000000..1f68691a2b7 --- /dev/null +++ b/core/modules/rest/src/PathProcessor/PathProcessorEntityResourceBC.php @@ -0,0 +1,55 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public function processInbound($path, Request $request) { + if ($request->getMethod() === 'POST' && strpos($path, '/entity/') === 0) { + $parts = explode('/', $path); + $entity_type_id = array_pop($parts); + + // Until Drupal 8.3, no entity types specified a link template for the + // 'create' link relation type. As of Drupal 8.3, all core content entity + // types provide this link relation type. This inbound path processor + // provides automatic backwards compatibility: it allows both the old + // default from \Drupal\rest\Plugin\rest\resource\EntityResource, i.e. + // "/entity/{entity_type}" and the link template specified in a particular + // entity type. The former is rewritten to the latter + // specific one if it exists. + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + if ($entity_type->hasLinkTemplate('create')) { + return $entity_type->getLinkTemplate('create'); + } + } + return $path; + } + +} diff --git a/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php b/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php index 010fad7363e..a74e8b2a61f 100644 --- a/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php +++ b/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php @@ -74,7 +74,7 @@ class EntityDeriver implements ContainerDeriverInterface { $default_uris = [ 'canonical' => "/entity/$entity_type_id/" . '{' . $entity_type_id . '}', - 'https://www.drupal.org/link-relations/create' => "/entity/$entity_type_id", + 'create' => "/entity/$entity_type_id", ]; foreach ($default_uris as $link_relation => $default_uri) { diff --git a/core/modules/rest/src/Plugin/ResourceBase.php b/core/modules/rest/src/Plugin/ResourceBase.php index 50d8d758ced..b40a03109b0 100644 --- a/core/modules/rest/src/Plugin/ResourceBase.php +++ b/core/modules/rest/src/Plugin/ResourceBase.php @@ -100,7 +100,15 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin $definition = $this->getPluginDefinition(); $canonical_path = isset($definition['uri_paths']['canonical']) ? $definition['uri_paths']['canonical'] : '/' . strtr($this->pluginId, ':', '/') . '/{id}'; - $create_path = isset($definition['uri_paths']['https://www.drupal.org/link-relations/create']) ? $definition['uri_paths']['https://www.drupal.org/link-relations/create'] : '/' . strtr($this->pluginId, ':', '/'); + $create_path = isset($definition['uri_paths']['create']) ? $definition['uri_paths']['create'] : '/' . strtr($this->pluginId, ':', '/'); + // BC: the REST module originally created the POST URL for a resource by + // reading the 'https://www.drupal.org/link-relations/create' URI path from + // the plugin annotation. For consistency with entity type definitions, that + // then changed to reading the 'create' URI path. For any REST Resource + // plugins that were using the old mechanism, we continue to support that. + if (!isset($definition['uri_paths']['create']) && isset($definition['uri_paths']['https://www.drupal.org/link-relations/create'])) { + $create_path = $definition['uri_paths']['https://www.drupal.org/link-relations/create']; + } $route_name = strtr($this->pluginId, ':', '.'); diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index a9e0ff0e867..5d9849ded41 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -35,7 +35,7 @@ use Symfony\Component\HttpKernel\Exception\HttpException; * deriver = "Drupal\rest\Plugin\Deriver\EntityDeriver", * uri_paths = { * "canonical" = "/entity/{entity_type}/{entity}", - * "https://www.drupal.org/link-relations/create" = "/entity/{entity_type}" + * "create" = "/entity/{entity_type}" * } * ) */ @@ -431,7 +431,7 @@ class EntityResource extends ResourceBase implements DependentPluginInterface { * @see https://tools.ietf.org/html/rfc5988#section-5 */ protected function addLinkHeaders(EntityInterface $entity, Response $response) { - foreach ($entity->getEntityType()->getLinkTemplates() as $relation_name => $link_template) { + foreach ($entity->uriRelationships() as $relation_name) { if ($this->linkRelationTypeManager->hasDefinition($relation_name)) { /** @var \Drupal\Core\Http\LinkRelationTypeInterface $link_relation_type */ $link_relation_type = $this->linkRelationTypeManager->createInstance($relation_name); diff --git a/core/modules/rest/src/Routing/ResourceRoutes.php b/core/modules/rest/src/Routing/ResourceRoutes.php index c21f13cd961..5ba4c5da62b 100644 --- a/core/modules/rest/src/Routing/ResourceRoutes.php +++ b/core/modules/rest/src/Routing/ResourceRoutes.php @@ -3,16 +3,18 @@ namespace Drupal\rest\Routing; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Routing\RouteSubscriberBase; +use Drupal\Core\Routing\RouteBuildEvent; +use Drupal\Core\Routing\RoutingEvents; use Drupal\rest\Plugin\Type\ResourcePluginManager; use Drupal\rest\RestResourceConfigInterface; use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Routing\RouteCollection; /** * Subscriber for REST-style routes. */ -class ResourceRoutes extends RouteSubscriberBase { +class ResourceRoutes implements EventSubscriberInterface { /** * The plugin manager for REST plugins. @@ -54,18 +56,18 @@ class ResourceRoutes extends RouteSubscriberBase { /** * Alters existing routes for a specific collection. * - * @param \Symfony\Component\Routing\RouteCollection $collection - * The route collection for adding routes. + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The route build event. * @return array */ - protected function alterRoutes(RouteCollection $collection) { + public function onDynamicRouteEvent(RouteBuildEvent $event) { // Iterate over all enabled REST resource config entities. /** @var \Drupal\rest\RestResourceConfigInterface[] $resource_configs */ $resource_configs = $this->resourceConfigStorage->loadMultiple(); foreach ($resource_configs as $resource_config) { if ($resource_config->status()) { $resource_routes = $this->getRoutesForResourceConfig($resource_config); - $collection->addCollection($resource_routes); + $event->getRouteCollection()->addCollection($resource_routes); } } } @@ -131,4 +133,12 @@ class ResourceRoutes extends RouteSubscriberBase { return $collection; } + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[RoutingEvents::DYNAMIC] = 'onDynamicRouteEvent'; + return $events; + } + } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 84a9e4a5109..b0f44eb4a72 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -679,8 +679,8 @@ abstract class EntityResourceTestBase extends ResourceTestBase { // missing ?_format query string. $response = $this->request('POST', $url, $request_options); $this->assertSame(415, $response->getStatusCode()); - $this->assertSame(['text/plain; charset=UTF-8'], $response->getHeader('Content-Type')); - $this->assertContains(htmlspecialchars('No "Content-Type" request header specified'), (string) $response->getBody()); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + $this->assertContains('A client error happened', (string) $response->getBody()); $url->setOption('query', ['_format' => static::$format]); @@ -814,6 +814,17 @@ abstract class EntityResourceTestBase extends ResourceTestBase { $this->assertSame([], $response->getHeader('Location')); } $this->assertFalse($response->hasHeader('X-Drupal-Cache')); + + // BC: old default POST URLs have their path updated by the inbound path + // processor \Drupal\rest\PathProcessor\PathProcessorEntityResourceBC to the + // new URL, which is derived from the 'create' link template if an entity + // type specifies it. + if ($this->entity->getEntityType()->hasLinkTemplate('create')) { + $this->entityStorage->load(static::$secondCreatedEntityId)->delete(); + $old_url = Url::fromUri('base:entity/' . static::$entityTypeId); + $response = $this->request('POST', $old_url, $request_options); + $this->assertResourceResponse(201, FALSE, $response); + } } /** @@ -851,7 +862,8 @@ abstract class EntityResourceTestBase extends ResourceTestBase { if ($has_canonical_url) { $this->assertSame(405, $response->getStatusCode()); $this->assertSame(['GET, POST, HEAD'], $response->getHeader('Allow')); - $this->assertSame(['text/plain; charset=UTF-8'], $response->getHeader('Content-Type')); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + $this->assertContains('A client error happened', (string) $response->getBody()); } else { $this->assertSame(404, $response->getStatusCode()); @@ -880,8 +892,8 @@ abstract class EntityResourceTestBase extends ResourceTestBase { // DX: 415 when no Content-Type request header. $response = $this->request('PATCH', $url, $request_options); $this->assertSame(415, $response->getStatusCode()); - $this->assertSame(['text/plain; charset=UTF-8'], $response->getHeader('Content-Type')); - $this->assertTrue(FALSE !== strpos((string) $response->getBody(), htmlspecialchars('No "Content-Type" request header specified'))); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + $this->assertContains('A client error happened', (string) $response->getBody()); $url->setOption('query', ['_format' => static::$format]); @@ -1039,7 +1051,8 @@ abstract class EntityResourceTestBase extends ResourceTestBase { if ($has_canonical_url) { $this->assertSame(405, $response->getStatusCode()); $this->assertSame(['GET, POST, HEAD'], $response->getHeader('Allow')); - $this->assertSame(['text/plain; charset=UTF-8'], $response->getHeader('Content-Type')); + $this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type')); + $this->assertContains('A client error happened', (string) $response->getBody()); } else { $this->assertSame(404, $response->getStatusCode()); @@ -1174,8 +1187,8 @@ abstract class EntityResourceTestBase extends ResourceTestBase { * The URL to POST to. */ protected function getEntityResourcePostUrl() { - $has_canonical_url = $this->entity->hasLinkTemplate('https://www.drupal.org/link-relations/create'); - return $has_canonical_url ? $this->entity->toUrl() : Url::fromUri('base:entity/' . static::$entityTypeId); + $has_create_url = $this->entity->hasLinkTemplate('create'); + return $has_create_url ? Url::fromUri('internal:' . $this->entity->getEntityType()->getLinkTemplate('create')) : Url::fromUri('base:entity/' . static::$entityTypeId); } /** diff --git a/core/modules/rest/tests/src/Functional/ResourceTestBase.php b/core/modules/rest/tests/src/Functional/ResourceTestBase.php index c9e4c443ebd..74aa144f145 100644 --- a/core/modules/rest/tests/src/Functional/ResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php @@ -321,6 +321,9 @@ abstract class ResourceTestBase extends BrowserTestBase { * 'http_errors = FALSE' request option, nor do we want them to have to * convert Drupal Url objects to strings. * + * We also don't want to follow redirects automatically, to ensure these tests + * are able to detect when redirects are added or removed. + * * @see \GuzzleHttp\ClientInterface::request() * * @param string $method @@ -334,6 +337,7 @@ abstract class ResourceTestBase extends BrowserTestBase { */ protected function request($method, Url $url, array $request_options) { $request_options[RequestOptions::HTTP_ERRORS] = FALSE; + $request_options[RequestOptions::ALLOW_REDIRECTS] = FALSE; $request_options = $this->decorateWithXdebugCookie($request_options); $client = $this->getSession()->getDriver()->getClient()->getClient(); return $client->request($method, $url->setAbsolute(TRUE)->toString(), $request_options); diff --git a/core/modules/system/src/Controller/Http4xxController.php b/core/modules/system/src/Controller/Http4xxController.php index f8c97e4f54a..ee8a9776d4d 100644 --- a/core/modules/system/src/Controller/Http4xxController.php +++ b/core/modules/system/src/Controller/Http4xxController.php @@ -9,6 +9,18 @@ use Drupal\Core\Controller\ControllerBase; */ class Http4xxController extends ControllerBase { + /** + * The default 4xx error content. + * + * @return array + * A render array containing the message to display for 4xx errors. + */ + public function on4xx() { + return [ + '#markup' => $this->t('A client error happened'), + ]; + } + /** * The default 401 content. * diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index ee9afc1516f..9cfe2ca54c2 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -22,6 +22,14 @@ system.404: requirements: _access: 'TRUE' +system.4xx: + path: '/system/4xx' + defaults: + _controller: '\Drupal\system\Controller\Http4xxController:on4xx' + _title: 'Client error' + requirements: + _access: 'TRUE' + system.admin: path: '/admin' defaults: diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php index f3ada1f6dfb..9f8c5b05216 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php @@ -48,6 +48,9 @@ use Drupal\user\UserInterface; * }, * field_ui_base_route = "entity.entity_test.admin_form", * ) + * + * Note that this entity type annotation intentionally omits the "create" link + * template. See https://www.drupal.org/node/2293697. */ class EntityTest extends ContentEntityBase implements EntityOwnerInterface { diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php index c668bd3ee80..e6cb5428b97 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php @@ -41,6 +41,7 @@ use Drupal\Core\Field\BaseFieldDefinition; * "add-form" = "/entity_test_with_bundle/add/{entity_test_bundle}", * "edit-form" = "/entity_test_with_bundle/{entity_test_with_bundle}/edit", * "delete-form" = "/entity_test_with_bundle/{entity_test_with_bundle}/delete", + * "create" = "/entity_test_with_bundle", * }, * ) */ diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php index 535fa8c4251..03491ab9622 100644 --- a/core/modules/taxonomy/src/Entity/Term.php +++ b/core/modules/taxonomy/src/Entity/Term.php @@ -46,6 +46,7 @@ use Drupal\taxonomy\TermInterface; * "canonical" = "/taxonomy/term/{taxonomy_term}", * "delete-form" = "/taxonomy/term/{taxonomy_term}/delete", * "edit-form" = "/taxonomy/term/{taxonomy_term}/edit", + * "create" = "/taxonomy/term", * }, * permission_granularity = "bundle" * )