Commit dd639772 authored by webchick's avatar webchick

Issue #2765959 by davidwbarratt, Wim Leers, dawehner, borisson_, tstoeckler,...

Issue #2765959 by davidwbarratt, Wim Leers, dawehner, borisson_, tstoeckler, tedbow, catch: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage
parent f9cde09b
......@@ -2,7 +2,7 @@
namespace Drupal\Tests\hal\Functional\EntityResource\ConfigurableLanguage;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\ConfigurableLanguage\ConfigurableLanguageResourceTestBase;
/**
......@@ -10,7 +10,7 @@
*/
class ConfigurableLanguageHalJsonBasicAuthTest extends ConfigurableLanguageResourceTestBase {
use BasicAuthResourceTestTrait;
use BasicAuthResourceWithInterfaceTranslationTestTrait;
/**
* {@inheritdoc}
......
......@@ -2,7 +2,7 @@
namespace Drupal\Tests\hal\Functional\EntityResource\ContentLanguageSettings;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\ContentLanguageSettings\ContentLanguageSettingsResourceTestBase;
/**
......@@ -10,7 +10,7 @@
*/
class ContentLanguageSettingsHalJsonBasicAuthTest extends ContentLanguageSettingsResourceTestBase {
use BasicAuthResourceTestTrait;
use BasicAuthResourceWithInterfaceTranslationTestTrait;
/**
* {@inheritdoc}
......
......@@ -37,6 +37,7 @@ public function __construct(RouteBuilderInterface $router_builder) {
*/
public function onSave(ConfigCrudEvent $event) {
$saved_config = $event->getConfig();
// @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
if ($saved_config->getName() === 'rest.settings' && $event->isChanged('bc_entity_resource_permissions')) {
$this->routerBuilder->setRebuildNeeded();
}
......
......@@ -12,6 +12,7 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException;
use Drupal\Core\TypedData\PrimitiveInterface;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
......@@ -122,7 +123,7 @@ public static function create(ContainerInterface $container, array $configuratio
public function get(EntityInterface $entity) {
$entity_access = $entity->access('view', NULL, TRUE);
if (!$entity_access->isAllowed()) {
throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
throw new CacheableAccessDeniedHttpException($entity_access, $entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
}
$response = new ResourceResponse($entity, 200);
......
......@@ -4,6 +4,7 @@
use Drupal\Component\Utility\ArgumentsResolver;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
......@@ -33,21 +34,34 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
*/
protected $resourceStorage;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Creates a new RequestHandler instance.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
* The resource configuration storage.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(EntityStorageInterface $entity_storage) {
public function __construct(EntityStorageInterface $entity_storage, ConfigFactoryInterface $config_factory) {
$this->resourceStorage = $entity_storage;
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('entity_type.manager')->getStorage('rest_resource_config'));
return new static(
$container->get('entity_type.manager')->getStorage('rest_resource_config'),
$container->get('config.factory')
);
}
/**
......@@ -133,8 +147,12 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
$response = call_user_func_array([$resource, $method], $arguments);
if ($response instanceof CacheableResponseInterface) {
// Add rest config's cache tags.
$response->addCacheableDependency($resource_config);
// Add global rest settings config's cache tag, for BC flags.
// @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
// @see \Drupal\rest\EventSubscriber\RestConfigSubscriber
// @todo Remove in https://www.drupal.org/node/2893804
$response->addCacheableDependency($this->configFactory->get('rest.settings'));
}
return $response;
......
......@@ -24,7 +24,7 @@ trait AnonResourceTestTrait {
/**
* {@inheritdoc}
*/
protected function assertResponseWhenMissingAuthentication(ResponseInterface $response) {
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
throw new \LogicException('When testing for anonymous users, authentication cannot be missing.');
}
......
......@@ -14,6 +14,8 @@
* authenticated, a 401 response must be sent.
* - Because every request must send an authorization, there is no danger of
* CSRF attacks.
*
* @see \Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait
*/
trait BasicAuthResourceTestTrait {
......@@ -31,8 +33,11 @@ protected function getAuthenticationRequestOptions($method) {
/**
* {@inheritdoc}
*/
protected function assertResponseWhenMissingAuthentication(ResponseInterface $response) {
$this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response);
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
$expected_page_cache_header_value = $method === 'GET' ? 'MISS' : FALSE;
// @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
$expected_dynamic_page_cache_header_value = $expected_page_cache_header_value;
$this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, ['4xx-response', 'config:system.site', 'config:user.role.anonymous', 'http_response'], ['user.roles:anonymous'], $expected_page_cache_header_value, $expected_dynamic_page_cache_header_value);
}
/**
......
<?php
namespace Drupal\Tests\rest\Functional;
use Psr\Http\Message\ResponseInterface;
/**
* Trait for ResourceTestBase subclasses testing $auth=basic_auth + 'language'.
*
* @see \Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait
*/
trait BasicAuthResourceWithInterfaceTranslationTestTrait {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
// Because BasicAuth::challengeException() relies on the 'system.site'
// configuration, and this test installs the 'language' module, all config
// may be translated and therefore gets the 'languages:language_interface'
// cache context.
$expected_page_cache_header_value = $method === 'GET' ? 'MISS' : FALSE;
$this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, ['4xx-response', 'config:system.site', 'config:user.role.anonymous', 'http_response'], ['languages:language_interface', 'user.roles:anonymous'], $expected_page_cache_header_value, $expected_page_cache_header_value);
}
}
......@@ -91,11 +91,31 @@ protected function getAuthenticationRequestOptions($method) {
/**
* {@inheritdoc}
*/
protected function assertResponseWhenMissingAuthentication(ResponseInterface $response) {
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
// Requests needing cookie authentication but missing it results in a 403
// response. The cookie authentication mechanism sets no response message.
// Hence, effectively, this is just the 403 response that one gets as the
// anonymous user trying to access a certain REST resource.
// @see \Drupal\user\Authentication\Provider\Cookie
// @todo https://www.drupal.org/node/2847623
$this->assertResourceErrorResponse(403, FALSE, $response);
if ($method === 'GET') {
$expected_cookie_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability();
// - \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber applies
// to cacheable anonymous responses: it updates their cacheability.
// - A 403 response to a GET request is cacheable.
// Therefore we must update our cacheability expectations accordingly.
if (in_array('user.permissions', $expected_cookie_403_cacheability->getCacheContexts(), TRUE)) {
$expected_cookie_403_cacheability->addCacheTags(['config:user.role.anonymous']);
}
// @todo Fix \Drupal\block\BlockAccessControlHandler::mergeCacheabilityFromConditions() in https://www.drupal.org/node/2867881
if (static::$entityTypeId === 'block') {
$expected_cookie_403_cacheability->setCacheTags(str_replace('user:2', 'user:0', $expected_cookie_403_cacheability->getCacheTags()));
}
$this->assertResourceErrorResponse(403, FALSE, $response, $expected_cookie_403_cacheability->getCacheTags(), $expected_cookie_403_cacheability->getCacheContexts(), 'MISS', 'MISS');
}
else {
$this->assertResourceErrorResponse(403, FALSE, $response);
}
}
/**
......
......@@ -141,4 +141,19 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
}
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {
// @see \Drupal\block\BlockAccessControlHandler::checkAccess()
return parent::getExpectedUnauthorizedAccessCacheability()
->setCacheTags([
'4xx-response',
'config:block.block.llama',
'http_response',
static::$auth ? 'user:2' : 'user:0',
])
->setCacheContexts(['user.roles']);
}
}
......@@ -171,4 +171,13 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {
// @see \Drupal\block_content\BlockContentAccessControlHandler()
return parent::getExpectedUnauthorizedAccessCacheability()
->addCacheTags(['block_content:1']);
}
}
......@@ -357,4 +357,13 @@ public function testPostSkipCommentApproval() {
$this->assertTrue($unserialized->getStatus());
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {
// @see \Drupal\comment\CommentAccessControlHandler::checkAccess()
return parent::getExpectedUnauthorizedAccessCacheability()
->addCacheTags(['comment:1']);
}
}
......@@ -2,14 +2,14 @@
namespace Drupal\Tests\rest\Functional\EntityResource\ConfigurableLanguage;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
/**
* @group rest
*/
class ConfigurableLanguageJsonBasicAuthTest extends ConfigurableLanguageResourceTestBase {
use BasicAuthResourceTestTrait;
use BasicAuthResourceWithInterfaceTranslationTestTrait;
/**
* {@inheritdoc}
......
......@@ -2,7 +2,7 @@
namespace Drupal\Tests\rest\Functional\EntityResource\ConfigurableLanguage;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
......@@ -10,7 +10,7 @@
*/
class ConfigurableLanguageXmlBasicAuthTest extends ConfigurableLanguageResourceTestBase {
use BasicAuthResourceTestTrait;
use BasicAuthResourceWithInterfaceTranslationTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
......
......@@ -2,14 +2,14 @@
namespace Drupal\Tests\rest\Functional\EntityResource\ContentLanguageSettings;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
/**
* @group rest
*/
class ContentLanguageSettingsJsonBasicAuthTest extends ContentLanguageSettingsResourceTestBase {
use BasicAuthResourceTestTrait;
use BasicAuthResourceWithInterfaceTranslationTestTrait;
/**
* {@inheritdoc}
......
......@@ -2,7 +2,7 @@
namespace Drupal\Tests\rest\Functional\EntityResource\ContentLanguageSettings;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
......@@ -10,7 +10,7 @@
*/
class ContentLanguageSettingsXmlBasicAuthTest extends ContentLanguageSettingsResourceTestBase {
use BasicAuthResourceTestTrait;
use BasicAuthResourceWithInterfaceTranslationTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
......
......@@ -261,4 +261,13 @@ public function testPost() {
$this->markTestSkipped('POSTing File Media items is not supported until https://www.drupal.org/node/1927648 is solved.');
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {
// @see \Drupal\media\MediaAccessControlHandler::checkAccess()
return parent::getExpectedUnauthorizedAccessCacheability()
->addCacheTags(['media:1']);
}
}
......@@ -99,4 +99,13 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
}
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {
// @see \Drupal\search\SearchPageAccessControlHandler::checkAccess()
return parent::getExpectedUnauthorizedAccessCacheability()
->addCacheTags(['config:search.page.hinode_search']);
}
}
......@@ -159,14 +159,14 @@ public function testPatchDxForSecuritySensitiveBaseFields() {
// DX: 422 when changing email without providing the password.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response);
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response, FALSE, FALSE, FALSE, FALSE);
$normalization['pass'] = [['existing' => 'wrong']];
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
// DX: 422 when changing email while providing a wrong password.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response);
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response, FALSE, FALSE, FALSE, FALSE);
$normalization['pass'] = [['existing' => $this->account->passRaw]];
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
......@@ -183,7 +183,7 @@ public function testPatchDxForSecuritySensitiveBaseFields() {
// DX: 422 when changing password without providing the current password.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n", $response);
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n", $response, FALSE, FALSE, FALSE, FALSE);
$normalization['pass'][0]['existing'] = $this->account->pass_raw;
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
......@@ -263,4 +263,13 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
}
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {
// @see \Drupal\user\UserAccessControlHandler::checkAccess()
return parent::getExpectedUnauthorizedAccessCacheability()
->addCacheTags(['user:3']);
}
}
......@@ -214,8 +214,13 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
/**
* Verifies the error response in case of missing authentication.
*
* @param string $method
* HTTP method.
* @param \Psr\Http\Message\ResponseInterface $response
* The response to assert.
*/
abstract protected function assertResponseWhenMissingAuthentication(ResponseInterface $response);
abstract protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response);
/**
* Asserts normalization-specific edge cases.
......@@ -249,6 +254,14 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
*/
abstract protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options);
/**
* Returns the expected cacheability of an unauthorized access response.
*
* @return \Drupal\Core\Cache\RefinableCacheableDependencyInterface
* The expected cacheability.
*/
abstract protected function getExpectedUnauthorizedAccessCacheability();
/**
* Initializes authentication.
*
......@@ -348,12 +361,67 @@ protected function request($method, Url $url, array $request_options) {
* The expected response body. FALSE in case this should not be asserted.
* @param \Psr\Http\Message\ResponseInterface $response
* The response to assert.
* @param string[]|false $expected_cache_tags
* (optional) The expected cache tags in the X-Drupal-Cache-Tags response
* header, or FALSE if that header should be absent. Defaults to FALSE.
* @param string[]|false $expected_cache_contexts
* (optional) The expected cache contexts in the X-Drupal-Cache-Contexts
* response header, or FALSE if that header should be absent. Defaults to
* FALSE.
* @param string|false $expected_page_cache_header_value
* (optional) The expected X-Drupal-Cache response header value, or FALSE if
* that header should be absent. Possible strings: 'MISS', 'HIT'. Defaults
* to FALSE.
* @param string|false $expected_dynamic_page_cache_header_value
* (optional) The expected X-Drupal-Dynamic-Cache response header value, or
* FALSE if that header should be absent. Possible strings: 'MISS', 'HIT'.
* Defaults to FALSE.
*/
protected function assertResourceResponse($expected_status_code, $expected_body, ResponseInterface $response) {
protected function assertResourceResponse($expected_status_code, $expected_body, ResponseInterface $response, $expected_cache_tags = FALSE, $expected_cache_contexts = FALSE, $expected_page_cache_header_value = FALSE, $expected_dynamic_page_cache_header_value = FALSE) {
$this->assertSame($expected_status_code, $response->getStatusCode());
$this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
if ($expected_body !== FALSE) {
$this->assertSame($expected_body, (string) $response->getBody());
if ($expected_status_code === 204) {
// DELETE responses should not include a Content-Type header. But Apache
// sets it to 'text/html' by default. We also cannot detect the presence
// of Apache either here in the CLI. For now having this documented here
// is all we can do.
// $this->assertSame(FALSE, $response->hasHeader('Content-Type'));
$this->assertSame('', (string) $response->getBody());
}
else {
$this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
if ($expected_body !== FALSE) {
$this->assertSame($expected_body, (string) $response->getBody());
}
}
// Expected cache tags: X-Drupal-Cache-Tags header.
$this->assertSame($expected_cache_tags !== FALSE, $response->hasHeader('X-Drupal-Cache-Tags'));
if (is_array($expected_cache_tags)) {
$this->assertSame($expected_cache_tags, explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0]));
}
// Expected cache contexts: X-Drupal-Cache-Contexts header.
$this->assertSame($expected_cache_contexts !== FALSE, $response->hasHeader('X-Drupal-Cache-Contexts'));
if (is_array($expected_cache_contexts)) {
$this->assertSame($expected_cache_contexts, explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0]));
}
// Expected Page Cache header value: X-Drupal-Cache header.
if ($expected_page_cache_header_value !== FALSE) {
$this->assertTrue($response->hasHeader('X-Drupal-Cache'));
$this->assertSame($expected_page_cache_header_value, $response->getHeader('X-Drupal-Cache')[0]);
}
else {
$this->assertFalse($response->hasHeader('X-Drupal-Cache'));
}
// Expected Dynamic Page Cache header value: X-Drupal-Dynamic-Cache header.
if ($expected_dynamic_page_cache_header_value !== FALSE) {
$this->assertTrue($response->hasHeader('X-Drupal-Dynamic-Cache'));
$this->assertSame($expected_dynamic_page_cache_header_value, $response->getHeader('X-Drupal-Dynamic-Cache')[0]);
}
else {
$this->assertFalse($response->hasHeader('X-Drupal-Dynamic-Cache'));
}
}
......@@ -366,10 +434,25 @@ protected function assertResourceResponse($expected_status_code, $expected_body,
* The expected error message.
* @param \Psr\Http\Message\ResponseInterface $response
* The error response to assert.
* @param string[]|false $expected_cache_tags
* (optional) The expected cache tags in the X-Drupal-Cache-Tags response
* header, or FALSE if that header should be absent. Defaults to FALSE.
* @param string[]|false $expected_cache_contexts
* (optional) The expected cache contexts in the X-Drupal-Cache-Contexts
* response header, or FALSE if that header should be absent. Defaults to
* FALSE.
* @param string|false $expected_page_cache_header_value
* (optional) The expected X-Drupal-Cache response header value, or FALSE if
* that header should be absent. Possible strings: 'MISS', 'HIT'. Defaults
* to FALSE.
* @param string|false $expected_dynamic_page_cache_header_value
* (optional) The expected X-Drupal-Dynamic-Cache response header value, or
* FALSE if that header should be absent. Possible strings: 'MISS', 'HIT'.
* Defaults to FALSE.
*/
protected function assertResourceErrorResponse($expected_status_code, $expected_message, ResponseInterface $response) {
protected function assertResourceErrorResponse($expected_status_code, $expected_message, ResponseInterface $response, $expected_cache_tags = FALSE, $expected_cache_contexts = FALSE, $expected_page_cache_header_value = FALSE, $expected_dynamic_page_cache_header_value = FALSE) {
$expected_body = ($expected_message !== FALSE) ? $this->serializer->encode(['message' => $expected_message], static::$format) : FALSE;
$this->assertResourceResponse($expected_status_code, $expected_body, $response);
$this->assertResourceResponse($expected_status_code, $expected_body, $response, $expected_cache_tags, $expected_cache_contexts, $expected_page_cache_header_value, $expected_dynamic_page_cache_header_value);
}
/**
......
......@@ -2,6 +2,8 @@
namespace Drupal\Tests\rest\Kernel;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Routing\RouteMatch;
use Drupal\KernelTests\KernelTestBase;
......@@ -40,7 +42,10 @@ class RequestHandlerTest extends KernelTestBase {
public function setUp() {
parent::setUp();
$this->entityStorage = $this->prophesize(EntityStorageInterface::class);
$this->requestHandler = new RequestHandler($this->entityStorage->reveal());
$config_factory = $this->prophesize(ConfigFactoryInterface::class);
$config_factory->get('rest.settings')
->willReturn($this->prophesize(ImmutableConfig::class)->reveal());
$this->requestHandler = new RequestHandler($this->entityStorage->reveal(), $config_factory->reveal());
$this->requestHandler->setContainer($this->container);
}
......
......@@ -2,6 +2,8 @@
namespace Drupal\serialization\EventSubscriber;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableResponse;
use Drupal\Core\EventSubscriber\HttpExceptionSubscriberBase;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
......@@ -51,8 +53,12 @@ protected function getHandledFormats() {
*/
protected static function getPriority() {
// This will fire after the most common HTML handler, since HTML requests
// are still more common than HTTP requests.
return -75;
// are still more common than HTTP requests. But it has a lower priority
// than \Drupal\Core\EventSubscriber\ExceptionJsonSubscriber::on4xx(), so
// that this also handles the 'json' format. Then all serialization formats
// (::getHandledFormats()) are handled by this exception subscriber, which
// results in better consistency.
return -70;
}
/**
......@@ -67,14 +73,22 @@ public function on4xx(GetResponseForExceptionEvent $event) {
$request = $event->getRequest();
$format = $request->getRequestFormat();
$content = ['message' => $event->getException()->getMessage()];
$content = ['message' => $exception->getMessage()];
$encoded_content = $this->serializer->serialize($content, $format);
$headers = $exception->getHeaders();
// Add the MIME type from the request to send back in the header.
$headers['Content-Type'] = $request->getMimeType($format);
$response = new Response($encoded_content, $exception->getStatusCode(), $headers);
// If the exception is cacheable, generate a cacheable response.
if ($exception instanceof CacheableDependencyInterface) {
$response = new CacheableResponse($encoded_content, $exception->getStatusCode(), $headers);
$response->addCacheableDependency($exception);
}
else {
$response = new Response($encoded_content, $exception->getStatusCode(), $headers);
}
$event->setResponse($response);
}
......
......@@ -58,7 +58,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
return AccessResult::allowed()->cachePerUser();
}
else {
return AccessResultNeutral::neutral("The 'access user profiles' permission is required and the user must be active.");
return AccessResultNeutral::neutral("The 'access user profiles' permission is required and the user must be active.")->cachePerPermissions()->addCacheableDependency($entity);
}
break;
......
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