diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php index 4007003..4728c52 100644 --- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php @@ -115,13 +115,22 @@ public function onRespond(FilterResponseEvent $event) { $response->headers->set('X-Content-Type-Options', 'nosniff', FALSE); $response->headers->set('X-Frame-Options', 'SAMEORIGIN', FALSE); + // If the current response isn't an implementation of the + // CacheableResponseInterface, we assume that a Response is either + // explicitly not cacheable or that caching headers are already set in + // another place. + if (!$response instanceof CacheableResponseInterface) { + if (!$this->isCacheControlCustomized($response)) { + $this->setResponseNotCacheable($response, $request); + } + return; + } + // Expose the cache contexts and cache tags associated with this page in a // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively. - if ($response instanceof CacheableResponseInterface) { - $response_cacheability = $response->getCacheableMetadata(); - $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $response_cacheability->getCacheTags())); - $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContextsManager->optimizeTokens($response_cacheability->getCacheContexts()))); - } + $response_cacheability = $response->getCacheableMetadata(); + $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $response_cacheability->getCacheTags())); + $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContextsManager->optimizeTokens($response_cacheability->getCacheContexts()))); $is_cacheable = ($this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) && ($this->responsePolicy->check($response, $request) !== ResponsePolicyInterface::DENY); diff --git a/core/modules/image/src/Controller/ImageStyleDownloadController.php b/core/modules/image/src/Controller/ImageStyleDownloadController.php index 2cc6585..7b67072 100644 --- a/core/modules/image/src/Controller/ImageStyleDownloadController.php +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php @@ -123,7 +123,16 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st // control access to the file. if ($scheme == 'private') { if (file_exists($derivative_uri)) { - return parent::download($request, $scheme); + $response = parent::download($request, $scheme); + + if ($response instanceof BinaryFileResponse) { + // The expires header is usually set in the finish response subscriber + // but not for images so we're setting it here. + $response->headers->add([ + 'Expires' => \DateTime::createFromFormat('D, d M Y H:i:s T', 'Sun, 19 Nov 1978 05:00:00 GMT')->format('D, d M Y H:i:s T') + ]); + } + return $response; } else { $headers = $this->moduleHandler()->invokeAll('file_download', array($image_uri)); @@ -174,11 +183,14 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st if ($success) { $image = $this->imageFactory->get($derivative_uri); $uri = $image->getSource(); + // The expires header is usually set in the finish response subscriber + // but not for images so we're setting it here. $headers += array( 'Content-Type' => $image->getMimeType(), 'Content-Length' => $image->getFileSize(), + 'Expires' => \DateTime::createFromFormat('D, d M Y H:i:s T', 'Sun, 19 Nov 1978 05:00:00 GMT')->format('D, d M Y H:i:s T'), ); - return new BinaryFileResponse($uri, 200, $headers); + return new BinaryFileResponse($uri, 200, $headers, FALSE); } else { $this->logger->notice('Unable to generate the derived image located at %path.', array('%path' => $derivative_uri)); diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php index 18a6dfd..a2278e7 100644 --- a/core/modules/page_cache/src/Tests/PageCacheTest.php +++ b/core/modules/page_cache/src/Tests/PageCacheTest.php @@ -406,4 +406,50 @@ public function testFormImmutability() { $this->assertText("Immutable: FALSE", "Form is not immutable,"); } + /** + * Tests cacheability of a CacheableResponse. + * + * Tests the difference between having a controller return a plain Symfony + * Response object versus returning a Response object that implements the + * CacheableResponseInterface. + */ + public function testCacheableResponseResponses() { + $config = $this->config('system.performance'); + $config->set('cache.page.max_age', 300); + $config->save(); + + // Try to fill the cache. + $this->drupalGet('/system-test/respond-reponse'); + $this->assertFalse(in_array('X-Drupal-Cache', $this->drupalGetHeaders()), 'Drupal page cache header not found'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'must-revalidate, no-cache, post-check=0, pre-check=0, private', 'Cache-Control header was sent'); + + // Still not cached, uncacheable response. + $this->drupalGet('/system-test/respond-reponse'); + $this->assertFalse(in_array('X-Drupal-Cache', $this->drupalGetHeaders()), 'Drupal page cache header not found'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'must-revalidate, no-cache, post-check=0, pre-check=0, private', 'Cache-Control header was sent'); + + // Try to fill the cache. + $this->drupalGet('/system-test/respond-cacheable-reponse'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'max-age=300, public', 'Cache-Control header was sent.'); + + // Should be cached now. + $this->drupalGet('/system-test/respond-cacheable-reponse'); + $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'max-age=300, public', 'Cache-Control header was sent.'); + + // Uninstall page cache. This should flush all caches so the next call to a + // previously cached page should be a miss now. + $this->container->get('module_installer') + ->uninstall(['page_cache']); + + // Try to fill the cache. + $this->drupalGet('/system-test/respond-reponse'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'must-revalidate, no-cache, post-check=0, pre-check=0, private', 'Cache-Control header was sent'); + + // Still not cached, uncacheable response. + $this->drupalGet('/system-test/respond-reponse'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'must-revalidate, no-cache, post-check=0, pre-check=0, private', 'Cache-Control header was sent'); + } + } diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php index ee4b890..d3b69b5 100644 --- a/core/modules/rest/src/RequestHandler.php +++ b/core/modules/rest/src/RequestHandler.php @@ -7,6 +7,7 @@ namespace Drupal\rest; +use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\Render\RenderContext; use Drupal\Core\Routing\RouteMatchInterface; use Symfony\Component\DependencyInjection\ContainerAwareInterface; @@ -67,7 +68,7 @@ public function handle(RouteMatchInterface $route_match, Request $request) { catch (UnexpectedValueException $e) { $error['error'] = $e->getMessage(); $content = $serializer->serialize($error, $format); - return new Response($content, 400, array('Content-Type' => $request->getMimeType($format))); + return new Response($content, 400, ['Content-Type' => $request->getMimeType($format)]); } } else { diff --git a/core/modules/system/src/FileDownloadController.php b/core/modules/system/src/FileDownloadController.php index a65342d..65ad3de 100644 --- a/core/modules/system/src/FileDownloadController.php +++ b/core/modules/system/src/FileDownloadController.php @@ -52,17 +52,11 @@ public function download(Request $request, $scheme = 'private') { // Let other modules provide headers and controls access to the file. $headers = $this->moduleHandler()->invokeAll('file_download', array($uri)); - foreach ($headers as $result) { - if ($result == -1) { - throw new AccessDeniedHttpException(); - } + if (in_array(-1, $headers) || empty($headers)) { + throw new AccessDeniedHttpException(); } - if (count($headers)) { - return new BinaryFileResponse($uri, 200, $headers); - } - - throw new AccessDeniedHttpException(); + return new BinaryFileResponse($uri, 200, $headers, $scheme !== 'private'); } throw new NotFoundHttpException(); diff --git a/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php b/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php index c24d7c5..a77c472 100644 --- a/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php +++ b/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php @@ -7,9 +7,9 @@ namespace Drupal\system_test\Controller; +use Drupal\Core\Cache\CacheableJsonResponse; +use Drupal\Core\Cache\CacheableResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Response; /** * Defines a controller to respond the page cache accept header test. @@ -26,10 +26,10 @@ class PageCacheAcceptHeaderController { */ public function content(Request $request) { if ($request->getRequestFormat() === 'json') { - return new JsonResponse(array('content' => 'oh hai this is json')); + return new CacheableJsonResponse(['content' => 'oh hai this is json']); } else { - return new Response("

oh hai this is html.

"); + return new CacheableResponse("

oh hai this is html.

"); } } } diff --git a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php index 15ab478..4ef189a 100644 --- a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php +++ b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php @@ -8,6 +8,7 @@ namespace Drupal\system_test\Controller; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Render\SafeString; @@ -259,13 +260,22 @@ public function authorizeInit($page_title) { */ public function setHeader(Request $request) { $query = $request->query->all(); - $response = new Response(); + $response = new CacheableResponse(); $response->headers->set($query['name'], $query['value']); + $response->getCacheableMetadata()->addCacheContexts(['url.query_args:name', 'url.query_args:value']); $response->setContent($this->t('The following header was set: %name: %value', array('%name' => $query['name'], '%value' => $query['value']))); return $response; } + public function respondWithReponse(Request $request) { + return new Response('test'); + } + + public function respondWithCacheableReponse(Request $request) { + return new CacheableResponse('test'); + } + /** * A simple page callback which adds a register shutdown function. */ diff --git a/core/modules/system/tests/modules/system_test/system_test.routing.yml b/core/modules/system/tests/modules/system_test/system_test.routing.yml index ecb9921..94a1bef 100644 --- a/core/modules/system/tests/modules/system_test/system_test.routing.yml +++ b/core/modules/system/tests/modules/system_test/system_test.routing.yml @@ -129,3 +129,17 @@ system_test.permission_dependent_route_access: _controller: '\Drupal\system_test\Controller\SystemTestController::mainContentFallback' requirements: _permission: 'pet llamas' + +system_test.respond_response: + path: '/system-test/respond-reponse' + defaults: + _controller: '\Drupal\system_test\Controller\SystemTestController::respondWithReponse' + requirements: + _access: 'TRUE' + +system_test.respond_cacheable_response: + path: '/system-test/respond-cacheable-reponse' + defaults: + _controller: '\Drupal\system_test\Controller\SystemTestController::respondWithCacheableReponse' + requirements: + _access: 'TRUE'