summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Pott2017-05-15 13:05:40 +0100
committerAlex Pott2017-05-15 13:05:40 +0100
commit43981094712d4f3e716b919e7b59ae8e46c109c3 (patch)
tree48f704a609d21f3c5bf0ebbd74156044e079dc2b
parent09096d781952c9b716a8b619bb0f671a3c0c610c (diff)
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
-rw-r--r--core/core.services.yml2
-rw-r--r--core/lib/Drupal/Core/Entity/Entity.php15
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php12
-rw-r--r--core/modules/block_content/src/Entity/BlockContent.php1
-rw-r--r--core/modules/comment/src/Entity/Comment.php1
-rw-r--r--core/modules/node/src/Entity/Node.php1
-rw-r--r--core/modules/rest/rest.services.yml12
-rw-r--r--core/modules/rest/src/EventSubscriber/EntityResourcePostRouteSubscriber.php74
-rw-r--r--core/modules/rest/src/PathProcessor/PathProcessorEntityResourceBC.php55
-rw-r--r--core/modules/rest/src/Plugin/Deriver/EntityDeriver.php2
-rw-r--r--core/modules/rest/src/Plugin/ResourceBase.php10
-rw-r--r--core/modules/rest/src/Plugin/rest/resource/EntityResource.php4
-rw-r--r--core/modules/rest/src/Routing/ResourceRoutes.php22
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php29
-rw-r--r--core/modules/rest/tests/src/Functional/ResourceTestBase.php4
-rw-r--r--core/modules/system/src/Controller/Http4xxController.php12
-rw-r--r--core/modules/system/system.routing.yml8
-rw-r--r--core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php3
-rw-r--r--core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php1
-rw-r--r--core/modules/taxonomy/src/Entity/Term.php1
20 files changed, 249 insertions, 20 deletions
diff --git a/core/core.services.yml b/core/core.services.yml
index 30fbcc2..fc0d99f 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 f36f74f..95e85e2 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 38cb895..f168a02 100644
--- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
@@ -80,6 +80,18 @@ class DefaultExceptionHtmlSubscriber extends HttpExceptionSubscriberBase {
}
/**
+ * 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.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
diff --git a/core/modules/block_content/src/Entity/BlockContent.php b/core/modules/block_content/src/Entity/BlockContent.php
index b7c3d4d..76071df 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 a5f1966..821b30d 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 b66628c..74a7c0b 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 869eb19..080a681 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 0000000..36a3f73
--- /dev/null
+++ b/core/modules/rest/src/EventSubscriber/EntityResourcePostRouteSubscriber.php
@@ -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;
+ }
+
+}
diff --git a/core/modules/rest/src/PathProcessor/PathProcessorEntityResourceBC.php b/core/modules/rest/src/PathProcessor/PathProcessorEntityResourceBC.php
new file mode 100644
index 0000000..1f68691
--- /dev/null
+++ b/core/modules/rest/src/PathProcessor/PathProcessorEntityResourceBC.php
@@ -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;
+ }
+
+}
diff --git a/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php b/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php
index 010fad7..a74e8b2 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 50d8d75..b40a031 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 a9e0ff0..5d9849d 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 c21f13c..5ba4c5d 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 84a9e4a..b0f44eb 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 c9e4c44..74aa144 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 f8c97e4..ee8a977 100644
--- a/core/modules/system/src/Controller/Http4xxController.php
+++ b/core/modules/system/src/Controller/Http4xxController.php
@@ -10,6 +10,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.
*
* @return array
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index ee9afc1..9cfe2ca 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 f3ada1f..9f8c5b0 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 c668bd3..e6cb542 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 535fa8c..03491ab 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"
* )