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

8.4.x
Alex Pott 2017-05-15 13:05:40 +01:00
parent 09096d7819
commit 4398109471
20 changed files with 249 additions and 20 deletions

View File

@ -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']

View File

@ -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;
});
}
/**

View File

@ -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.
*

View File

@ -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 = {

View File

@ -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",

View File

@ -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",
* }
* )
*/

View File

@ -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 }

View File

@ -0,0 +1,74 @@
<?php
namespace Drupal\rest\EventSubscriber;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Generates a 'create' route for an entity type if it has a REST POST route.
*/
class EntityResourcePostRouteSubscriber implements EventSubscriberInterface {
/**
* The REST resource config storage.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $resourceConfigStorage;
/**
* Constructs a new EntityResourcePostRouteSubscriber instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->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;
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Drupal\rest\PathProcessor;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Path processor to maintain BC for entity REST resource URLs from Drupal 8.0.
*/
class PathProcessorEntityResourceBC implements InboundPathProcessorInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Creates a new PathProcessorEntityResourceBC instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->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;
}
}

View File

@ -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) {

View File

@ -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, ':', '.');

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);
}
/**

View File

@ -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);

View File

@ -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.
*

View File

@ -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:

View File

@ -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 {

View File

@ -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",
* },
* )
*/

View File

@ -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"
* )