summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreffulgentsia2017-11-08 19:31:27 -0800
committereffulgentsia2017-11-08 19:31:27 -0800
commitb83782a74b0fa73c9e172537fbd52b7155e49909 (patch)
treec704ba46b2d28079ba4da0c095f055c1f954b48e
parent52a598e2bf739b39709bd1fd77237e99e8572017 (diff)
Issue #2920001 by Wim Leers, dawehner, borisson_, aheimlich, davidwbarratt: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata
-rw-r--r--core/lib/Drupal/Core/Cache/CacheableDependencyTrait.php67
-rw-r--r--core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php42
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php8
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php13
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableAccessDeniedHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableBadRequestHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableConflictHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableGoneHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableLengthRequiredHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableMethodNotAllowedHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableNotAcceptableHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableNotFoundHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheablePreconditionFailedHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheablePreconditionRequiredHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableServiceUnavailableHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableTooManyRequestsHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableUnauthorizedHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableUnprocessableEntityHttpException.php24
-rw-r--r--core/lib/Drupal/Core/Http/Exception/CacheableUnsupportedMediaTypeHttpException.php24
-rw-r--r--core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php31
-rw-r--r--core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php42
-rw-r--r--core/modules/rest/tests/modules/rest_test/rest_test.services.yml5
-rw-r--r--core/modules/rest/tests/modules/rest_test/src/PageCache/RequestPolicy/DenyTestAuthRequests.php31
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php15
-rw-r--r--core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionJsonSubscriberTest.php25
26 files changed, 610 insertions, 53 deletions
diff --git a/core/lib/Drupal/Core/Cache/CacheableDependencyTrait.php b/core/lib/Drupal/Core/Cache/CacheableDependencyTrait.php
new file mode 100644
index 0000000..19cb0f2
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/CacheableDependencyTrait.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\Core\Cache;
+
+/**
+ * Trait for \Drupal\Core\Cache\CacheableDependencyInterface.
+ */
+trait CacheableDependencyTrait {
+
+ /**
+ * Cache contexts.
+ *
+ * @var string[]
+ */
+ protected $cacheContexts = [];
+
+ /**
+ * Cache tags.
+ *
+ * @var string[]
+ */
+ protected $cacheTags = [];
+
+ /**
+ * Cache max-age.
+ *
+ * @var int
+ */
+ protected $cacheMaxAge = Cache::PERMANENT;
+
+ /**
+ * Sets cacheability; useful for value object constructors.
+ *
+ * @param \Drupal\Core\Cache\CacheableDependencyInterface $cacheability
+ * The cacheability to set.
+ *
+ * @return $this
+ */
+ protected function setCacheability(CacheableDependencyInterface $cacheability) {
+ $this->cacheContexts = $cacheability->getCacheContexts();
+ $this->cacheTags = $cacheability->getCacheTags();
+ $this->cacheMaxAge = $cacheability->getCacheMaxAge();
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheTags() {
+ return $this->cacheTags;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheContexts() {
+ return $this->cacheContexts;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheMaxAge() {
+ return $this->cacheMaxAge;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php b/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php
index 21b61b2..fcbc11f 100644
--- a/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php
+++ b/core/lib/Drupal/Core/Cache/RefinableCacheableDependencyTrait.php
@@ -7,47 +7,7 @@ namespace Drupal\Core\Cache;
*/
trait RefinableCacheableDependencyTrait {
- /**
- * Cache contexts.
- *
- * @var string[]
- */
- protected $cacheContexts = [];
-
- /**
- * Cache tags.
- *
- * @var string[]
- */
- protected $cacheTags = [];
-
- /**
- * Cache max-age.
- *
- * @var int
- */
- protected $cacheMaxAge = Cache::PERMANENT;
-
- /**
- * {@inheritdoc}
- */
- public function getCacheTags() {
- return $this->cacheTags;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getCacheContexts() {
- return $this->cacheContexts;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getCacheMaxAge() {
- return $this->cacheMaxAge;
- }
+ use CacheableDependencyTrait;
/**
* {@inheritdoc}
diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
index f168a02..8812583 100644
--- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
@@ -2,6 +2,7 @@
namespace Drupal\Core\EventSubscriber;
+use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Drupal\Core\Utility\Error;
use Psr\Log\LoggerInterface;
@@ -170,6 +171,13 @@ class DefaultExceptionHtmlSubscriber extends HttpExceptionSubscriberBase {
$response->setStatusCode($status_code);
}
+ // Persist the exception's cacheability metadata, if any. If the exception
+ // itself isn't cacheable, then this will make the response uncacheable:
+ // max-age=0 will be set.
+ if ($response instanceof CacheableResponseInterface) {
+ $response->addCacheableDependency($exception);
+ }
+
// Persist any special HTTP headers that were set on the exception.
if ($exception instanceof HttpExceptionInterface) {
$response->headers->add($exception->getHeaders());
diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
index 6ccb743..239e582 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
@@ -2,6 +2,8 @@
namespace Drupal\Core\EventSubscriber;
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableJsonResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
@@ -35,7 +37,16 @@ class ExceptionJsonSubscriber extends HttpExceptionSubscriberBase {
public function on4xx(GetResponseForExceptionEvent $event) {
/** @var \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $exception */
$exception = $event->getException();
- $response = new JsonResponse(['message' => $event->getException()->getMessage()], $exception->getStatusCode(), $exception->getHeaders());
+
+ // If the exception is cacheable, generate a cacheable response.
+ if ($exception instanceof CacheableDependencyInterface) {
+ $response = new CacheableJsonResponse(['message' => $event->getException()->getMessage()], $exception->getStatusCode(), $exception->getHeaders());
+ $response->addCacheableDependency($exception);
+ }
+ else {
+ $response = new JsonResponse(['message' => $event->getException()->getMessage()], $exception->getStatusCode(), $exception->getHeaders());
+ }
+
$event->setResponse($response);
}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableAccessDeniedHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableAccessDeniedHttpException.php
new file mode 100644
index 0000000..e0fd5cb
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableAccessDeniedHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+
+/**
+ * A cacheable AccessDeniedHttpException.
+ */
+class CacheableAccessDeniedHttpException extends AccessDeniedHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableBadRequestHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableBadRequestHttpException.php
new file mode 100644
index 0000000..97d432a
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableBadRequestHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * A cacheable BadRequestHttpException.
+ */
+class CacheableBadRequestHttpException extends BadRequestHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableConflictHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableConflictHttpException.php
new file mode 100644
index 0000000..ca804fb
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableConflictHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
+
+/**
+ * A cacheable ConflictHttpException.
+ */
+class CacheableConflictHttpException extends ConflictHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableGoneHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableGoneHttpException.php
new file mode 100644
index 0000000..4568c91
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableGoneHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\GoneHttpException;
+
+/**
+ * A cacheable GoneHttpException.
+ */
+class CacheableGoneHttpException extends GoneHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableHttpException.php
new file mode 100644
index 0000000..76f529e
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * A cacheable HttpException.
+ */
+class CacheableHttpException extends HttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $statusCode = 0, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($statusCode, $message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableLengthRequiredHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableLengthRequiredHttpException.php
new file mode 100644
index 0000000..a75f80a
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableLengthRequiredHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException;
+
+/**
+ * A cacheable LengthRequiredHttpException.
+ */
+class CacheableLengthRequiredHttpException extends LengthRequiredHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableMethodNotAllowedHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableMethodNotAllowedHttpException.php
new file mode 100644
index 0000000..d9919b1
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableMethodNotAllowedHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
+
+/**
+ * A cacheable MethodNotAllowedHttpException.
+ */
+class CacheableMethodNotAllowedHttpException extends MethodNotAllowedHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, array $allow, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($allow, $message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableNotAcceptableHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableNotAcceptableHttpException.php
new file mode 100644
index 0000000..94bf1c2
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableNotAcceptableHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
+
+/**
+ * A cacheable NotAcceptableHttpException.
+ */
+class CacheableNotAcceptableHttpException extends NotAcceptableHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableNotFoundHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableNotFoundHttpException.php
new file mode 100644
index 0000000..9e5e136
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableNotFoundHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * A cacheable NotFoundHttpException.
+ */
+class CacheableNotFoundHttpException extends NotFoundHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionFailedHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionFailedHttpException.php
new file mode 100644
index 0000000..7921d3e
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionFailedHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
+
+/**
+ * A cacheable PreconditionFailedHttpException.
+ */
+class CacheablePreconditionFailedHttpException extends PreconditionFailedHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionRequiredHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionRequiredHttpException.php
new file mode 100644
index 0000000..d66b255
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheablePreconditionRequiredHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException;
+
+/**
+ * A cacheable PreconditionRequiredHttpException.
+ */
+class CacheablePreconditionRequiredHttpException extends PreconditionRequiredHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableServiceUnavailableHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableServiceUnavailableHttpException.php
new file mode 100644
index 0000000..313b9ae
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableServiceUnavailableHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
+
+/**
+ * A cacheable ServiceUnavailableHttpException.
+ */
+class CacheableServiceUnavailableHttpException extends ServiceUnavailableHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $retryAfter = NULL, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($retryAfter, $message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableTooManyRequestsHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableTooManyRequestsHttpException.php
new file mode 100644
index 0000000..e709c0b
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableTooManyRequestsHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
+
+/**
+ * A cacheable TooManyRequestsHttpException.
+ */
+class CacheableTooManyRequestsHttpException extends TooManyRequestsHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $retryAfter = NULL, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($retryAfter, $message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableUnauthorizedHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableUnauthorizedHttpException.php
new file mode 100644
index 0000000..35dbd72
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableUnauthorizedHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
+
+/**
+ * A cacheable UnauthorizedHttpException.
+ */
+class CacheableUnauthorizedHttpException extends UnauthorizedHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $challenge, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($challenge, $message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableUnprocessableEntityHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableUnprocessableEntityHttpException.php
new file mode 100644
index 0000000..655c67a
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableUnprocessableEntityHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
+
+/**
+ * A cacheable UnprocessableEntityHttpException.
+ */
+class CacheableUnprocessableEntityHttpException extends UnprocessableEntityHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($message, $previous, $code);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Http/Exception/CacheableUnsupportedMediaTypeHttpException.php b/core/lib/Drupal/Core/Http/Exception/CacheableUnsupportedMediaTypeHttpException.php
new file mode 100644
index 0000000..c6f6023
--- /dev/null
+++ b/core/lib/Drupal/Core/Http/Exception/CacheableUnsupportedMediaTypeHttpException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Core\Http\Exception;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
+
+/**
+ * A cacheable UnsupportedMediaTypeHttpException.
+ */
+class CacheableUnsupportedMediaTypeHttpException extends UnsupportedMediaTypeHttpException implements CacheableDependencyInterface {
+
+ use CacheableDependencyTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(CacheableDependencyInterface $cacheability, $message = NULL, \Exception $previous = NULL, $code = 0) {
+ $this->setCacheability($cacheability);
+ parent::__construct($message, $previous, $code);
+ }
+
+}
diff --git a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
index c72e3f0..01034f4 100644
--- a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
+++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
@@ -5,12 +5,13 @@ namespace Drupal\basic_auth\Authentication\Provider;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Drupal\Core\Authentication\AuthenticationProviderChallengeInterface;
+use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Flood\FloodInterface;
+use Drupal\Core\Http\Exception\CacheableUnauthorizedHttpException;
use Drupal\user\UserAuthInterface;
use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
/**
* HTTP Basic authentication provider.
@@ -126,11 +127,35 @@ class BasicAuth implements AuthenticationProviderInterface, AuthenticationProvid
* {@inheritdoc}
*/
public function challengeException(Request $request, \Exception $previous) {
- $site_name = $this->configFactory->get('system.site')->get('name');
+ $site_config = $this->configFactory->get('system.site');
+ $site_name = $site_config->get('name');
$challenge = SafeMarkup::format('Basic realm="@realm"', [
'@realm' => !empty($site_name) ? $site_name : 'Access restricted',
]);
- return new UnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
+
+ // A 403 is converted to a 401 here, but it doesn't matter what the
+ // cacheability was of the 403 exception: what matters here is that
+ // authentication credentials are missing, i.e. that this request was made
+ // as the anonymous user.
+ // Therefore, all we must do, is make this response:
+ // 1. vary by whether the current user has the 'anonymous' role or not. This
+ // works fine because:
+ // - Thanks to \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests,
+ // Page Cache never caches a response whose request has Basic Auth
+ // credentials.
+ // - Dynamic Page Cache will cache a different result for when the
+ // request is unauthenticated (this 401) versus authenticated (some
+ // other response)
+ // 2. have the 'config:user.role.anonymous' cache tag, because the only
+ // reason this 401 would no longer be a 401 is if permissions for the
+ // 'anonymous' role change, causing that cache tag to be invalidated.
+ // @see \Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge()
+ // @see \Drupal\Core\EventSubscriber\ClientErrorResponseSubscriber()
+ // @see \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onAllResponds()
+ $cacheability = CacheableMetadata::createFromObject($site_config)
+ ->addCacheTags(['config:user.role.anonymous'])
+ ->addCacheContexts(['user.roles:anonymous']);
+ return new CacheableUnauthorizedHttpException($cacheability, (string) $challenge, 'No authentication credentials provided.', $previous);
}
}
diff --git a/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php b/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php
index b197a76..f844cb9 100644
--- a/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php
+++ b/core/modules/basic_auth/tests/src/Functional/BasicAuthTest.php
@@ -7,6 +7,7 @@ use Drupal\Core\Url;
use Drupal\Tests\basic_auth\Traits\BasicAuthTestTrait;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
+use Drupal\user\Entity\Role;
/**
* Tests for BasicAuth authentication provider.
@@ -181,6 +182,47 @@ class BasicAuthTest extends BrowserTestBase {
}
/**
+ * Tests the cacheability of Basic Auth's 401 response.
+ *
+ * @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
+ */
+ public function testCacheabilityOf401Response() {
+ $session = $this->getSession();
+ $url = Url::fromRoute('router_test.11');
+
+ $assert_response_cacheability = function ($expected_page_cache_header_value, $expected_dynamic_page_cache_header_value) use ($session, $url) {
+ $this->drupalGet($url);
+ $this->assertSession()->statusCodeEquals(401);
+ $this->assertSame($expected_page_cache_header_value, $session->getResponseHeader('X-Drupal-Cache'));
+ $this->assertSame($expected_dynamic_page_cache_header_value, $session->getResponseHeader('X-Drupal-Dynamic-Cache'));
+ };
+
+ // 1. First request: cold caches, both Page Cache and Dynamic Page Cache are
+ // now primed.
+ $assert_response_cacheability('MISS', 'MISS');
+ // 2. Second request: Page Cache HIT, we don't even hit Dynamic Page Cache.
+ // This is going to keep happening.
+ $assert_response_cacheability('HIT', 'MISS');
+ // 3. Third request: after clearing Page Cache, we now see that Dynamic Page
+ // Cache is a HIT too.
+ $this->container->get('cache.page')->deleteAll();
+ $assert_response_cacheability('MISS', 'HIT');
+ // 4. Fourth request: warm caches.
+ $assert_response_cacheability('HIT', 'HIT');
+
+ // If the permissions of the 'anonymous' role change, it may no longer be
+ // necessary to be authenticated to access this route. Therefore the cached
+ // 401 responses should be invalidated.
+ $this->grantPermissions(Role::load(Role::ANONYMOUS_ID), [$this->randomMachineName()]);
+ $assert_response_cacheability('MISS', 'MISS');
+ $assert_response_cacheability('HIT', 'MISS');
+ // Idem for when the 'system.site' config changes.
+ $this->config('system.site')->save();
+ $assert_response_cacheability('MISS', 'MISS');
+ $assert_response_cacheability('HIT', 'MISS');
+ }
+
+ /**
* Tests if the controller is called before authentication.
*
* @see https://www.drupal.org/node/2817727
diff --git a/core/modules/rest/tests/modules/rest_test/rest_test.services.yml b/core/modules/rest/tests/modules/rest_test/rest_test.services.yml
index ccdbeae..d316cf6 100644
--- a/core/modules/rest/tests/modules/rest_test/rest_test.services.yml
+++ b/core/modules/rest/tests/modules/rest_test/rest_test.services.yml
@@ -7,3 +7,8 @@ services:
class: Drupal\rest_test\Authentication\Provider\TestAuthGlobal
tags:
- { name: authentication_provider, provider_id: 'rest_test_auth_global', global: TRUE }
+ rest_test.page_cache_request_policy.deny_test_auth_requests:
+ class: Drupal\rest_test\PageCache\RequestPolicy\DenyTestAuthRequests
+ public: false
+ tags:
+ - { name: page_cache_request_policy }
diff --git a/core/modules/rest/tests/modules/rest_test/src/PageCache/RequestPolicy/DenyTestAuthRequests.php b/core/modules/rest/tests/modules/rest_test/src/PageCache/RequestPolicy/DenyTestAuthRequests.php
new file mode 100644
index 0000000..17be647
--- /dev/null
+++ b/core/modules/rest/tests/modules/rest_test/src/PageCache/RequestPolicy/DenyTestAuthRequests.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\rest_test\PageCache\RequestPolicy;
+
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Cache policy for pages requested with REST Test Auth.
+ *
+ * This policy disallows caching of requests that use the REST Test Auth
+ * authentication provider for security reasons (just like basic_auth).
+ * Otherwise responses for authenticated requests can get into the page cache
+ * and could be delivered to unprivileged users.
+ *
+ * @see \Drupal\rest_test\Authentication\Provider\TestAuth
+ * @see \Drupal\rest_test\Authentication\Provider\TestAuthGlobal
+ * @see \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests
+ */
+class DenyTestAuthRequests implements RequestPolicyInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function check(Request $request) {
+ if ($request->headers->has('REST-test-auth') || $request->headers->has('REST-test-auth-global')) {
+ return self::DENY;
+ }
+ }
+
+}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 6faf028..418ddfb 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -414,14 +414,20 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
':pattern' => '%[route]=rest.%',
])
->fetchAllAssoc('cid');
- $this->assertCount(2, $cache_items);
+ $this->assertTrue(count($cache_items) >= 2);
$found_cache_redirect = FALSE;
- $found_cached_response = FALSE;
+ $found_cached_200_response = FALSE;
+ $other_cached_responses_are_4xx = TRUE;
foreach ($cache_items as $cid => $cache_item) {
$cached_data = unserialize($cache_item->data);
if (!isset($cached_data['#cache_redirect'])) {
- $found_cached_response = TRUE;
$cached_response = $cached_data['#response'];
+ if ($cached_response->getStatusCode() === 200) {
+ $found_cached_200_response = TRUE;
+ }
+ elseif (!$cached_response->isClientError()) {
+ $other_cached_responses_are_4xx = FALSE;
+ }
$this->assertNotInstanceOf(ResourceResponseInterface::class, $cached_response);
$this->assertInstanceOf(CacheableResponseInterface::class, $cached_response);
}
@@ -430,7 +436,8 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
}
}
$this->assertTrue($found_cache_redirect);
- $this->assertTrue($found_cached_response);
+ $this->assertTrue($found_cached_200_response);
+ $this->assertTrue($other_cached_responses_are_4xx);
}
$cache_tags_header_value = $response->getHeader('X-Drupal-Cache-Tags')[0];
$this->assertEquals($this->getExpectedCacheTags(), empty($cache_tags_header_value) ? [] : explode(' ', $cache_tags_header_value));
diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionJsonSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionJsonSubscriberTest.php
index 86c6d86..8f339a8 100644
--- a/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionJsonSubscriberTest.php
+++ b/core/tests/Drupal/Tests/Core/EventSubscriber/ExceptionJsonSubscriberTest.php
@@ -2,11 +2,15 @@
namespace Drupal\Tests\Core\EventSubscriber;
+use Drupal\Core\Cache\CacheableJsonResponse;
+use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\EventSubscriber\ExceptionJsonSubscriber;
+use Drupal\Core\Http\Exception\CacheableMethodNotAllowedHttpException;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
@@ -18,21 +22,34 @@ class ExceptionJsonSubscriberTest extends UnitTestCase {
/**
* @covers ::on4xx
+ * @dataProvider providerTestOn4xx
*/
- public function testOn4xx() {
+ public function testOn4xx(HttpExceptionInterface $exception, $expected_response_class) {
$kernel = $this->prophesize(HttpKernelInterface::class);
$request = Request::create('/test');
- $e = new MethodNotAllowedHttpException(['POST', 'PUT'], 'test message');
- $event = new GetResponseForExceptionEvent($kernel->reveal(), $request, 'GET', $e);
+ $event = new GetResponseForExceptionEvent($kernel->reveal(), $request, 'GET', $exception);
$subscriber = new ExceptionJsonSubscriber();
$subscriber->on4xx($event);
$response = $event->getResponse();
- $this->assertInstanceOf(JsonResponse::class, $response);
+ $this->assertInstanceOf($expected_response_class, $response);
$this->assertEquals('{"message":"test message"}', $response->getContent());
$this->assertEquals(405, $response->getStatusCode());
$this->assertEquals('POST, PUT', $response->headers->get('Allow'));
$this->assertEquals('application/json', $response->headers->get('Content-Type'));
}
+ public function providerTestOn4xx() {
+ return [
+ 'uncacheable exception' => [
+ new MethodNotAllowedHttpException(['POST', 'PUT'], 'test message'),
+ JsonResponse::class
+ ],
+ 'cacheable exception' => [
+ new CacheableMethodNotAllowedHttpException((new CacheableMetadata())->setCacheContexts(['route']), ['POST', 'PUT'], 'test message'),
+ CacheableJsonResponse::class
+ ],
+ ];
+ }
+
}