core/composer.json | 1 + core/core.services.yml | 3 + .../ContentControllerSubscriber.php | 2 +- core/lib/Drupal/Core/Form/FormBuilder.php | 11 + core/lib/Drupal/Core/Render/RenderCache.php | 3 + .../src/Plugin/Block/TestAccessBlock.php | 2 +- core/modules/book/src/Tests/BookTest.php | 4 +- core/modules/book/tests/modules/book_test.module | 1 + core/modules/page_cache/page_cache.info.yml | 2 +- core/modules/page_cache/page_cache.module | 6 +- .../modules/page_cache/src/Tests/PageCacheTest.php | 2 +- core/modules/path/src/Tests/PathAliasTest.php | 4 + core/modules/smart_cache/smart_cache.info.yml | 6 + core/modules/smart_cache/smart_cache.module | 27 ++ core/modules/smart_cache/smart_cache.services.yml | 32 +++ .../src/EventSubscriber/SmartCacheSubscriber.php | 316 +++++++++++++++++++++ .../RequestPolicy/DefaultRequestPolicy.php | 29 ++ .../PageCache/ResponsePolicy/DenyAdminRoutes.php | 51 ++++ .../src/Tests/SmartCacheIntegrationTest.php | 126 ++++++++ .../smart_cache_test/smart_cache_test.info.yml | 6 + .../smart_cache_test/smart_cache_test.routing.yml | 75 +++++ .../src/SmartCacheTestController.php | 139 +++++++++ .../system/src/EventSubscriber/ConfigCacheTag.php | 4 +- core/modules/system/system.module | 2 +- .../paramconverter_test/src/TestControllers.php | 4 +- core/profiles/standard/src/Tests/StandardTest.php | 31 ++ core/profiles/testing/testing.info.yml | 5 +- sites/example.settings.local.php | 15 +- 28 files changed, 894 insertions(+), 15 deletions(-) diff --git a/core/composer.json b/core/composer.json index b1547df..fcc97c3 100644 --- a/core/composer.json +++ b/core/composer.json @@ -105,6 +105,7 @@ "drupal/seven": "self.version", "drupal/shortcut": "self.version", "drupal/simpletest": "self.version", + "drupal/smart_cache": "self.version", "drupal/standard": "self.version", "drupal/stark": "self.version", "drupal/statistics": "self.version", diff --git a/core/core.services.yml b/core/core.services.yml index ed1341c..1945dab 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -250,17 +250,20 @@ services: class: Drupal\Core\PageCache\ResponsePolicy\KillSwitch tags: - { name: page_cache_response_policy } + - { name: smart_cache_response_policy } page_cache_no_cache_routes: class: Drupal\Core\PageCache\ResponsePolicy\DenyNoCacheRoutes arguments: ['@current_route_match'] public: false tags: - { name: page_cache_response_policy } + - { name: smart_cache_response_policy } page_cache_no_server_error: class: Drupal\Core\PageCache\ResponsePolicy\NoServerError public: false tags: - { name: page_cache_response_policy } + - { name: smart_cache_response_policy } config.manager: class: Drupal\Core\Config\ConfigManager arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage', '@event_dispatcher'] diff --git a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php index 16e9613..f6f30fe 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ContentControllerSubscriber.php @@ -40,7 +40,7 @@ public function onRequestDeriveFormWrapper(GetResponseEvent $event) { * An array of event listener definitions. */ static function getSubscribedEvents() { - $events[KernelEvents::REQUEST][] = array('onRequestDeriveFormWrapper', 29); + $events[KernelEvents::REQUEST][] = array('onRequestDeriveFormWrapper', 25); return $events; } diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index a9ae1bd..4949a7b 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -650,11 +650,22 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) { $form['#action'] = $this->buildFormAction(); } + // If the form method is specified in the form, pass it on to FormState. + if (isset($form['#method'])) { + $form_state->setMethod($form['#method']); + } + // Fix the form method, if it is 'get' in $form_state, but not in $form. if ($form_state->isMethodType('get') && !isset($form['#method'])) { $form['#method'] = 'get'; } + // Mark every non-GET form as uncacheable. + // @todo Refine in https://www.drupal.org/node/2526472. + if (!$form_state->isMethodType('get')) { + $form['#cache']['max-age'] = 0; + } + // Generate a new #build_id for this form, if none has been set already. // The form_build_id is used as key to cache a particular build of the form. // For multi-step forms, this allows the user to go back to an earlier diff --git a/core/lib/Drupal/Core/Render/RenderCache.php b/core/lib/Drupal/Core/Render/RenderCache.php index 880fca3..37be19d 100644 --- a/core/lib/Drupal/Core/Render/RenderCache.php +++ b/core/lib/Drupal/Core/Render/RenderCache.php @@ -16,6 +16,9 @@ /** * Wraps the caching logic for the render caching system. + * + * @todo Refactor this out into a generic service capable of cache redirects, + * let RenderCache use that. https://www.drupal.org/node/2551419 */ class RenderCache implements RenderCacheInterface { diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php index 873a77b..eb9ee56 100644 --- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestAccessBlock.php @@ -63,7 +63,7 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ protected function blockAccess(AccountInterface $account) { - return $this->state->get('test_block_access', FALSE) ? AccessResult::allowed() : AccessResult::forbidden(); + return $this->state->get('test_block_access', FALSE) ? AccessResult::allowed()->setCacheMaxAge(0) : AccessResult::forbidden()->setCacheMaxAge(0); } /** diff --git a/core/modules/book/src/Tests/BookTest.php b/core/modules/book/src/Tests/BookTest.php index b0235c4..b530265 100644 --- a/core/modules/book/src/Tests/BookTest.php +++ b/core/modules/book/src/Tests/BookTest.php @@ -8,6 +8,7 @@ namespace Drupal\book\Tests; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\EntityInterface; use Drupal\simpletest\WebTestBase; use Drupal\user\RoleInterface; @@ -124,11 +125,12 @@ public function testBookNavigationCacheContext() { // Enable the debug output. \Drupal::state()->set('book_test.debug_book_navigation_cache_context', TRUE); + Cache::invalidateTags(['book_test.debug_book_navigation_cache_context']); $this->drupalLogin($this->bookAuthor); // On non-node route. - $this->drupalGet(''); + $this->drupalGet($this->adminUser->urlInfo()); $this->assertRaw('[route.book_navigation]=book.none'); // On non-book node route. diff --git a/core/modules/book/tests/modules/book_test.module b/core/modules/book/tests/modules/book_test.module index 2f868a4..939e756 100644 --- a/core/modules/book/tests/modules/book_test.module +++ b/core/modules/book/tests/modules/book_test.module @@ -15,6 +15,7 @@ * Implements hook_page_attachments(). */ function book_test_page_attachments(array &$page) { + $page['#cache']['tags'][] = 'book_test.debug_book_navigation_cache_context'; if (\Drupal::state()->get('book_test.debug_book_navigation_cache_context', FALSE)) { drupal_set_message(\Drupal::service('cache_contexts_manager')->convertTokensToKeys(['route.book_navigation'])->getKeys()[0]); } diff --git a/core/modules/page_cache/page_cache.info.yml b/core/modules/page_cache/page_cache.info.yml index 4affed7..6999544 100644 --- a/core/modules/page_cache/page_cache.info.yml +++ b/core/modules/page_cache/page_cache.info.yml @@ -1,6 +1,6 @@ name: Internal Page Cache type: module -description: 'Caches pages for anonymous users. Works well for small to medium-sized websites.' +description: 'Caches entire pages for anonymous users. Works well for small to medium-sized websites.' package: Core version: VERSION core: 8.x diff --git a/core/modules/page_cache/page_cache.module b/core/modules/page_cache/page_cache.module index f4eb71b..e5f4f34 100644 --- a/core/modules/page_cache/page_cache.module +++ b/core/modules/page_cache/page_cache.module @@ -5,9 +5,8 @@ * Caches responses for anonymous users, request and response policies allowing. */ -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\PageCache\RequestPolicyInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Url; /** * Implements hook_help(). @@ -21,7 +20,8 @@ function page_cache_help($route_name, RouteMatchInterface $route_match) { $output .= '
'; $output .= '
' . t('Speeding up your site') . '
'; $output .= '
' . t('Pages requested by anonymous users are stored the first time they are requested and then are reused. Depending on your site configuration and the amount of your web traffic tied to anonymous visitors, the caching system may significantly increase the speed of your site.') . '
'; - $output .= '
' . t('Pages are usually identical for all anonymous users, while they can be customized for each authenticated user. This is why pages can be cached for anonymous users, whereas they will have to be rebuilt for every authenticated user.') . '
'; + $output .= '
' . t('Pages are usually identical for all anonymous users, while they can be personalized for each authenticated user. This is why pages can be cached for anonymous users, whereas they will have to be rebuilt for every authenticated user.') . '
'; + $output .= '
' . t('To speed up your site for authenticated users, see the Smart Cache module.', ['!smart_cache-help' => (\Drupal::moduleHandler()->moduleExists('smart_cache')) ? Url::fromRoute('help.page', ['name' => 'smart_cache'])->toString() : '#']) . '

'; $output .= '
' . t('Configuring the internal page cache') . '
'; $output .= '
' . t('On the Performance page, you can configure how long browsers and proxies may cache pages; that setting is also respected by the Internal Page Cache module. There is no other configuration.', array('!cache-settings' => \Drupal::url('system.performance_settings'))) . '
'; $output .= '
'; diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php index e97a1b4..18a6dfd 100644 --- a/core/modules/page_cache/src/Tests/PageCacheTest.php +++ b/core/modules/page_cache/src/Tests/PageCacheTest.php @@ -399,7 +399,7 @@ public function testFormImmutability() { // that implementation. \Drupal::state()->set('page_cache_bypass_form_immutability', TRUE); \Drupal::moduleHandler()->resetImplementations(); - \Drupal::cache('render')->deleteAll(); + Cache::invalidateTags(['rendered']); $this->drupalGet('page_cache_form_test_immutability'); diff --git a/core/modules/path/src/Tests/PathAliasTest.php b/core/modules/path/src/Tests/PathAliasTest.php index e4d07e2..651c11f 100644 --- a/core/modules/path/src/Tests/PathAliasTest.php +++ b/core/modules/path/src/Tests/PathAliasTest.php @@ -7,6 +7,8 @@ namespace Drupal\path\Tests; +use Drupal\Core\Cache\Cache; + /** * Add, edit, delete, and change alias and verify its consistency in the * database. @@ -57,6 +59,8 @@ function testPathCache() { // Visit the alias for the node and confirm a cache entry is created. \Drupal::cache('data')->deleteAll(); + // @todo Remove this once https://www.drupal.org/node/2480077 lands. + Cache::invalidateTags(['rendered']); $this->drupalGet(trim($edit['alias'], '/')); $this->assertTrue(\Drupal::cache('data')->get('preload-paths:' . $edit['source']), 'Cache entry was created.'); } diff --git a/core/modules/smart_cache/smart_cache.info.yml b/core/modules/smart_cache/smart_cache.info.yml new file mode 100644 index 0000000..cdac583 --- /dev/null +++ b/core/modules/smart_cache/smart_cache.info.yml @@ -0,0 +1,6 @@ +name: Smart Cache +type: module +description: 'Caches pages, minus the personalized parts. Works well for websites of all sizes.' +package: Core +version: VERSION +core: 8.x diff --git a/core/modules/smart_cache/smart_cache.module b/core/modules/smart_cache/smart_cache.module new file mode 100644 index 0000000..4c0a9e6 --- /dev/null +++ b/core/modules/smart_cache/smart_cache.module @@ -0,0 +1,27 @@ +' . t('About') . ''; + $output .= '

' . t('The Smart Cache module caches pages in the database, minus the personalized parts. For more information, see the online documentation for the Smart Cache module.', ['!smartcache-documentation' => 'https://www.drupal.org/documentation/modules/smart_cache']) . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
'; + $output .= '
' . t('Speeding up your site') . '
'; + $output .= '
' . t('Pages are stored the first time they are requested if they are safe to cache, and then are reused. Personalized parts are excluded automatically. Depending on your site configuration and the complexity of particular pages, Smart Cache may significantly increase the speed of your site, even for authenticated users.') . '
'; + $output .= '
' . t('The module requires no configuration. Every part of the page contains metadata that allows Smart Cache to figure this out on its own.') . '
'; + $output .= '
'; + + return $output; + } +} diff --git a/core/modules/smart_cache/smart_cache.services.yml b/core/modules/smart_cache/smart_cache.services.yml new file mode 100644 index 0000000..78fcb10 --- /dev/null +++ b/core/modules/smart_cache/smart_cache.services.yml @@ -0,0 +1,32 @@ +services: + # Cache bin. + cache.smart_cache: + class: Drupal\Core\Cache\CacheBackendInterface + tags: + - { name: cache.bin } + factory: cache_factory:get + arguments: [smart_cache] + + # Event subscriber. + smart_cache_subscriber: + class: Drupal\smart_cache\EventSubscriber\SmartCacheSubscriber + arguments: ['@smart_cache_request_policy', '@smart_cache_response_policy', '@render_cache', '%renderer.config%'] + tags: + - { name: event_subscriber } + + # Request & response policies. + smart_cache_request_policy: + class: Drupal\smart_cache\PageCache\RequestPolicy\DefaultRequestPolicy + tags: + - { name: service_collector, tag: smart_cache_request_policy, call: addPolicy} + smart_cache_response_policy: + class: Drupal\Core\PageCache\ChainResponsePolicy + tags: + - { name: service_collector, tag: smart_cache_response_policy, call: addPolicy} + lazy: true + smart_cache_deny_admin_routes: + class: Drupal\smart_cache\PageCache\ResponsePolicy\DenyAdminRoutes + arguments: ['@current_route_match'] + public: false + tags: + - { name: smart_cache_response_policy } diff --git a/core/modules/smart_cache/src/EventSubscriber/SmartCacheSubscriber.php b/core/modules/smart_cache/src/EventSubscriber/SmartCacheSubscriber.php new file mode 100644 index 0000000..9bed7cb --- /dev/null +++ b/core/modules/smart_cache/src/EventSubscriber/SmartCacheSubscriber.php @@ -0,0 +1,316 @@ + [ + 'keys' => ['response'], + 'contexts' => [ + 'route', + // Some routes' controllers rely on the request format (they don't have + // a separate route for each request format). Additionally, a controller + // may be returning a domain object that a KernelEvents::VIEW subscriber + // must turn into an actual response, but perhaps a format is being + // requested that the subscriber does not support. + // @see \Drupal\Core\EventSubscriber\AcceptNegotiation406::onViewDetect406() + 'request_format', + ], + 'bin' => 'smart_cache', + ], + ]; + + /** + * Constructs a new SmartCacheSubscriber object. + * + * @param \Drupal\Core\PageCache\RequestPolicyInterface $request_policy + * A policy rule determining the cacheability of a request. + * @param \Drupal\Core\PageCache\ResponsePolicyInterface $response_policy + * A policy rule determining the cacheability of the response. + * @param \Drupal\Core\Render\RenderCacheInterface $render_cache + * The render cache. + * @param array $renderer_config + * The renderer configuration array. + */ + public function __construct(RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, RenderCacheInterface $render_cache, array $renderer_config) { + $this->requestPolicy = $request_policy; + $this->responsePolicy = $response_policy; + $this->renderCache = $render_cache; + $this->rendererConfig = $renderer_config; + } + + /** + * Sets a response in case of a SmartCache cache hit. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The event to process. + */ + public function onRouteMatch(GetResponseEvent $event) { + // Don't cache the response if the SmartCache request policies are not met. + // Store the result in a request attribute, so that onResponse() does not + // have to redo the request policy check. + $request = $event->getRequest(); + $request_policy_result = $this->requestPolicy->check($request); + $request->attributes->set(self::ATTRIBUTE_REQUEST_POLICY_RESULT, $request_policy_result); + if ($request_policy_result === RequestPolicyInterface::DENY) { + return; + } + + // Sets the response for the current route, if cached. + $cached = $this->renderCache->get($this->smartCacheRedirectRenderArray); + if ($cached) { + $response = $this->renderArrayToResponse($cached); + $response->headers->set('X-Drupal-SmartCache', 'HIT'); + $event->setResponse($response); + } + } + + /** + * Stores a response in case of a SmartCache cache miss, if cacheable. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event to process. + */ + public function onResponse(FilterResponseEvent $event) { + $response = $event->getResponse(); + + // SmartCache only works with cacheable responses. It does not work with + // plain Response objects. (SmartCache needs to be able to access and modify + // the cacheability metadata associated with the response.) + if (!$response instanceof CacheableResponseInterface) { + return; + } + + // There's no work left to be done if this is a SmartCache cache hit. + if ($response->headers->get('X-Drupal-SmartCache') === 'HIT') { + return; + } + + // There's no work left to be done if this is an uncacheable response. + if (!$this->shouldCacheResponse($response)) { + // The response is uncacheable, mark it as such. + $response->headers->set('X-Drupal-SmartCache', 'UNCACHEABLE'); + return; + } + + // Don't cache the response if SmartCache's request subscriber did not fire, + // because that means it is impossible to have a SmartCache cache hit. + // (This can happen when the master request is for example a 403 or 404, in + // which case a subrequest is performed by the router. In that case, it is + // the subrequest's response that is cached by SmartCache, because the + // routing happens in a request subscriber earlier than SmartCache's and + // immediately sets a response, i.e. the one returned by the subrequest, and + // thus causes SmartCache's request subscriber to not fire for the master + // request.) + // @see \Drupal\Core\Routing\AccessAwareRouter::checkAccess() + // @see \Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber::on403() + $request = $event->getRequest(); + if (!$request->attributes->has(self::ATTRIBUTE_REQUEST_POLICY_RESULT)) { + return; + } + + // Don't cache the response if the SmartCache request & response policies + // are not met. + // @see onRouteMatch() + if ($request->attributes->get(self::ATTRIBUTE_REQUEST_POLICY_RESULT) === RequestPolicyInterface::DENY || $this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { + return; + } + + // Embed the response object in a render array so that RenderCache is able + // to cache it, handling cache redirection for us. + $response_as_render_array = $this->responseToRenderArray($response); + $this->renderCache->set($response_as_render_array, $this->smartCacheRedirectRenderArray); + + // The response was generated, mark the response as a cache miss. The next + // time, it will be a cache hit. + $response->headers->set('X-Drupal-SmartCache', 'MISS'); + } + + /** + * Whether the given response should be cached by SmartCache. + * + * We consider any response that has cacheability metadata meeting the auto- + * placeholdering conditions to be uncacheable. Because those conditions + * indicate poor cacheability, and if it doesn't make sense to cache parts of + * a page, then neither does it make sense to cache an entire page. + * + * @param \Drupal\Core\Cache\CacheableResponseInterface + * The response whose cacheability to analyze. + * + * @return bool + * Whether the given render array's cacheability does not meet the + * placeholdering conditions. + * + * @see \Drupal\Core\Render\Renderer::shouldAutomaticallyPlaceholder() + */ + protected function shouldCacheResponse(CacheableResponseInterface $response) { + $conditions = $this->rendererConfig['auto_placeholder_conditions']; + + $cacheability = $response->getCacheableMetadata(); + + // Response's max-age is at or below the configured threshold. + if ($cacheability->getCacheMaxAge() !== Cache::PERMANENT && $cacheability->getCacheMaxAge() <= $conditions['max-age']) { + return FALSE; + } + + // Response has a high-cardinality cache context. + if (array_intersect($cacheability->getCacheContexts(), $conditions['contexts'])) { + return FALSE; + } + + // Response has a high-invalidation frequency cache tag. + if (array_intersect($cacheability->getCacheTags(), $conditions['tags'])) { + return FALSE; + } + + return TRUE; + } + + /** + * Embeds a Response object in a render array to let RenderCache can cache it. + * + * @param \Drupal\Core\Cache\CacheableResponseInterface $response + * A cacheable response. + * + * @return array + * A render array that embeds the given cacheable response object, with the + * cacheability metadata of the response object present in the #cache + * property of the render array. + * + * @see renderArrayToResponse() + */ + protected function responseToRenderArray(CacheableResponseInterface $response) { + $response_as_render_array = $this->smartCacheRedirectRenderArray + [ + // The data we actually care about. + '#response' => $response, + // Tell RenderCache to cache the #response property: the data we actually + // care about. + '#cache_properties' => ['#response'], + // These exist only to fulfill the requirements of the RenderCache, which + // is designed to work with render arrays only. We don't care about these. + '#markup' => '', + '#attached' => '', + ]; + + // Merge the response's cacheability metadata, so that RenderCache can take + // care of cache redirects for us. + CacheableMetadata::createFromObject($response->getCacheableMetadata()) + ->merge(CacheableMetadata::createFromRenderArray($response_as_render_array)) + ->applyTo($response_as_render_array); + + return $response_as_render_array; + } + + /** + * Gets the embedded Response object in a render array. + * + * @param array $render_array + * A render array with a #response property. + * + * @return \Drupal\Core\Cache\CacheableResponseInterface + * The cacheable response object. + * + * @see responseToRenderArray() + */ + protected function renderArrayToResponse(array $render_array) { + return $render_array['#response']; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events = []; + + // Run after AuthenticationSubscriber (necessary for the 'user' cache + // context) and MaintenanceModeSubscriber (SmartCache should not be polluted + // by maintenance mode-specific behavior), but before + // ContentControllerSubscriber (updates _controller, but that is pointless + // when SmartCache runs). + $events[KernelEvents::REQUEST][] = ['onRouteMatch', 27]; + + // Run before HtmlResponseSubscriber::onRespond(), which has priority 0. + $events[KernelEvents::RESPONSE][] = ['onResponse', 100]; + + return $events; + } + +} diff --git a/core/modules/smart_cache/src/PageCache/RequestPolicy/DefaultRequestPolicy.php b/core/modules/smart_cache/src/PageCache/RequestPolicy/DefaultRequestPolicy.php new file mode 100644 index 0000000..0651b8f --- /dev/null +++ b/core/modules/smart_cache/src/PageCache/RequestPolicy/DefaultRequestPolicy.php @@ -0,0 +1,29 @@ +addPolicy(new CommandLineOrUnsafeMethod()); + } + +} diff --git a/core/modules/smart_cache/src/PageCache/ResponsePolicy/DenyAdminRoutes.php b/core/modules/smart_cache/src/PageCache/ResponsePolicy/DenyAdminRoutes.php new file mode 100644 index 0000000..b3458f8 --- /dev/null +++ b/core/modules/smart_cache/src/PageCache/ResponsePolicy/DenyAdminRoutes.php @@ -0,0 +1,51 @@ +routeMatch = $route_match; + } + + /** + * {@inheritdoc} + */ + public function check(Response $response, Request $request) { + if (($route = $this->routeMatch->getRouteObject()) && $route->getOption('_admin_route')) { + return static::DENY; + } + } + +} diff --git a/core/modules/smart_cache/src/Tests/SmartCacheIntegrationTest.php b/core/modules/smart_cache/src/Tests/SmartCacheIntegrationTest.php new file mode 100644 index 0000000..9956a08 --- /dev/null +++ b/core/modules/smart_cache/src/Tests/SmartCacheIntegrationTest.php @@ -0,0 +1,126 @@ +uninstall(['page_cache']); + } + + /** + * Tests that SmartCache works correctly, and verifies the edge cases. + */ + public function testSmartCache() { + // Controllers returning plain response objects are ignored by SmartCache. + $url = Url::fromUri('route:smart_cache_test.response'); + $this->drupalGet($url); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Response object returned: SmartCache is ignoring.'); + + // Controllers returning CacheableResponseInterface (cacheable response) + // objects are handled by SmartCache. + $url = Url::fromUri('route:smart_cache_test.cacheable_response'); + $this->drupalGet($url); + $this->assertEqual('MISS', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Cacheable response object returned: SmartCache is active, SmartCache MISS.'); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Cacheable response object returned: SmartCache is active, SmartCache HIT.'); + + // Controllers returning render arrays, rendered as HTML responses, are + // handled by SmartCache. + $url = Url::fromUri('route:smart_cache_test.html'); + $this->drupalGet($url); + $this->assertEqual('MISS', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache MISS.'); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache HIT.'); + + // The above is the simple case, where the render array returned by the + // response contains no cache contexts. So let's now test a route/controller + // that *does* vary by a cache context whose value we can easily control: it + // varies by the 'animal' query argument. + foreach (['llama', 'piggy', 'unicorn', 'kitten'] as $animal) { + $url = Url::fromUri('route:smart_cache_test.html.with_cache_contexts', ['query' => ['animal' => $animal]]); + $this->drupalGet($url); + $this->assertRaw($animal); + $this->assertEqual('MISS', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache MISS.'); + $this->drupalGet($url); + $this->assertRaw($animal); + $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache HIT.'); + + // Finally, let's also verify that the 'smart_cache_test.html' route + // continued to see cache hits if we specify a query argument, because it + // *should* ignore it and continue to provide SmartCache hits. + $url = Url::fromUri('route:smart_cache_test.html', ['query' => ['animal' => 'piglet']]); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response: SmartCache is active, SmartCache HIT.'); + } + + // Controllers returning render arrays, rendered as anything except a HTML + // response, are ignored by SmartCache (but only because those wrapper + // formats's responses do not implement CacheableResponseInterface). + $this->drupalGet('smart-cache-test/html', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax'))); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as AJAX response: SmartCache is ignoring.'); + $this->drupalGet('smart-cache-test/html', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_dialog'))); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as dialog response: SmartCache is ignoring.'); + $this->drupalGet('smart-cache-test/html', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal'))); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as modal response: SmartCache is ignoring.'); + + // Admin routes are ignored by SmartCache. + $this->drupalGet('smart-cache-test/html/admin'); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Response returned, rendered as HTML response, admin route: SmartCache is ignoring'); + $this->drupalGet('smart-cache-test/response/admin'); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Response returned, plain response, admin route: SmartCache is ignoring'); + $this->drupalGet('smart-cache-test/cacheable-response/admin'); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Response returned, cacheable response, admin route: SmartCache is ignoring'); + + // Max-age = 0 responses are ignored by SmartCache. + $this->drupalGet('smart-cache-test/html/uncacheable/max-age'); + $this->assertEqual('UNCACHEABLE', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response, but uncacheable: SmartCache is running, but not caching.'); + + // 'user' cache context responses are ignored by SmartCache. + $this->drupalGet('smart-cache-test/html/uncacheable/contexts'); + $this->assertEqual('UNCACHEABLE', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Render array returned, rendered as HTML response, but uncacheable: SmartCache is running, but not caching.'); + + // 'current-temperature' cache tag responses are ignored by SmartCache. + $this->drupalGet('smart-cache-test/html/uncacheable/tags'); + $this->assertEqual('MISS', $this->drupalGetHeader('X-Drupal-SmartCache'), 'By default, Drupal has no auto-placeholdering cache tags.'); + } + +} diff --git a/core/modules/smart_cache/tests/smart_cache_test/smart_cache_test.info.yml b/core/modules/smart_cache/tests/smart_cache_test/smart_cache_test.info.yml new file mode 100644 index 0000000..cfa52e2 --- /dev/null +++ b/core/modules/smart_cache/tests/smart_cache_test/smart_cache_test.info.yml @@ -0,0 +1,6 @@ +name: 'Test SmartCache' +type: module +description: 'Provides test routes/responses for SmartCache.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/smart_cache/tests/smart_cache_test/smart_cache_test.routing.yml b/core/modules/smart_cache/tests/smart_cache_test/smart_cache_test.routing.yml new file mode 100644 index 0000000..6bd84ac --- /dev/null +++ b/core/modules/smart_cache/tests/smart_cache_test/smart_cache_test.routing.yml @@ -0,0 +1,75 @@ +smart_cache_test.response: + path: '/smart-cache-test/response' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::response' + requirements: + _access: 'TRUE' + +smart_cache_test.response.admin: + path: '/smart-cache-test/response/admin' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::response' + requirements: + _access: 'TRUE' + options: + _admin_route: TRUE + +smart_cache_test.cacheable_response: + path: '/smart-cache-test/cacheable-response' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::cacheableResponse' + requirements: + _access: 'TRUE' + +smart_cache_test.cacheable_response.admin: + path: '/smart-cache-test/cacheable-response/admin' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::cacheableResponse' + requirements: + _access: 'TRUE' + options: + _admin_route: TRUE + +smart_cache_test.html: + path: '/smart-cache-test/html' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::html' + requirements: + _access: 'TRUE' + +smart_cache_test.html.admin: + path: '/smart-cache-test/html/admin' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::html' + requirements: + _access: 'TRUE' + options: + _admin_route: TRUE + +smart_cache_test.html.with_cache_contexts: + path: '/smart-cache-test/html/with-cache-contexts' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::htmlWithCacheContexts' + requirements: + _access: 'TRUE' + +smart_cache_test.html.uncacheable.max_age: + path: '/smart-cache-test/html/uncacheable/max-age' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::htmlUncacheableMaxAge' + requirements: + _access: 'TRUE' + +smart_cache_test.html.uncacheable.contexts: + path: '/smart-cache-test/html/uncacheable/contexts' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::htmlUncacheableContexts' + requirements: + _access: 'TRUE' + +smart_cache_test.html.uncacheable.tags: + path: '/smart-cache-test/html/uncacheable/tags' + defaults: + _controller: '\Drupal\smart_cache_test\SmartCacheTestController::htmlUncacheableTags' + requirements: + _access: 'TRUE' diff --git a/core/modules/smart_cache/tests/smart_cache_test/src/SmartCacheTestController.php b/core/modules/smart_cache/tests/smart_cache_test/src/SmartCacheTestController.php new file mode 100644 index 0000000..c1add68 --- /dev/null +++ b/core/modules/smart_cache/tests/smart_cache_test/src/SmartCacheTestController.php @@ -0,0 +1,139 @@ +label()); + $response->addCacheableDependency($user); + return $response; + } + + /** + * A route returning a render array (without cache contexts, so cacheable). + * + * @return array + * A render array. + */ + public function html() { + return [ + 'content' => [ + '#markup' => 'Hello world.', + ], + ]; + } + + /** + * A route returning a render array (with cache contexts, so cacheable). + * + * @return array + * A render array. + * + * @see html() + */ + public function htmlWithCacheContexts() { + $build = $this->html(); + $build['dynamic_part'] = [ + '#markup' => SafeMarkup::format('Hello there, %animal.', ['%animal' => \Drupal::requestStack()->getCurrentRequest()->query->get('animal')]), + '#cache' => [ + 'contexts' => [ + 'url.query_args:animal', + ], + ], + ]; + return $build; + } + + /** + * A route returning a render array (with max-age=0, so uncacheable) + * + * @return array + * A render array. + * + * @see html() + */ + public function htmlUncacheableMaxAge() { + $build = $this->html(); + $build['very_dynamic_part'] = [ + '#markup' => 'Drupal cannot handle the awesomeness of llamas.', + '#cache' => [ + 'max-age' => 0, + ], + ]; + return $build; + } + + /** + * A route returning a render array (with 'user' context, so uncacheable) + * + * @return array + * A render array. + * + * @see html() + */ + public function htmlUncacheableContexts() { + $build = $this->html(); + $build['very_dynamic_part'] = [ + '#markup' => 'Drupal cannot handle the awesomeness of llamas.', + '#cache' => [ + 'contexts' => [ + 'user', + ], + ], + ]; + return $build; + } + + /** + * A route returning a render array (with max-age=0, so uncacheable) + * + * @return array + * A render array. + * + * @see html() + */ + public function htmlUncacheableTags() { + $build = $this->html(); + $build['very_dynamic_part'] = [ + '#markup' => 'Drupal cannot handle the awesomeness of llamas.', + '#cache' => [ + 'tags' => [ + 'current-temperature', + ], + ], + ]; + return $build; + } + +} diff --git a/core/modules/system/src/EventSubscriber/ConfigCacheTag.php b/core/modules/system/src/EventSubscriber/ConfigCacheTag.php index 0482a74..640a6f0 100644 --- a/core/modules/system/src/EventSubscriber/ConfigCacheTag.php +++ b/core/modules/system/src/EventSubscriber/ConfigCacheTag.php @@ -60,8 +60,8 @@ public function onSave(ConfigCrudEvent $event) { $this->cacheTagsInvalidator->invalidateTags(['route_match', 'rendered']); } - // Global theme settings. - if ($event->getConfig()->getName() === 'system.theme.global') { + // Theme configuration and global theme settings. + if (in_array($event->getConfig()->getName(), ['system.theme', 'system.theme.global'])) { $this->cacheTagsInvalidator->invalidateTags(['rendered']); } diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 923e722..abf115a 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -90,7 +90,7 @@ function system_help($route_name, RouteMatchInterface $route_match) { $output .= '
' . t('Using maintenance mode') . '
'; $output .= '
' . t('When you are performing site maintenance, you can prevent non-administrative users (including anonymous visitors) from viewing your site by putting it in Maintenance mode. This will prevent unauthorized users from making changes to the site while you are performing maintenance, or from seeing a broken site while updates are in progress.', array('!maintenance-mode' => \Drupal::url('system.site_maintenance_mode'))) . '
'; $output .= '
' . t('Configuring for performance') . '
'; - $output .= '
' . t('On the Performance page, the site can be configured to aggregate CSS and JavaScript files, making the total request size smaller. Note that, for small- to medium-sized websites, the Internal Page Cache module should be installed so that pages are efficiently cached and reused.', array('!performance-page' => \Drupal::url('system.performance_settings'), '!page-cache' => (\Drupal::moduleHandler()->moduleExists('page_cache')) ? \Drupal::url('help.page', array('name' => 'page_cache')) : '#')) . '
'; + $output .= '
' . t('On the Performance page, the site can be configured to aggregate CSS and JavaScript files, making the total request size smaller. Note that, for small- to medium-sized websites, the Internal Page Cache module should be installed so that pages are efficiently cached and reused for anonymous users. Finally, for websites of all sizes, the Smart Cache module should be installed so that the non-personalized parts of pages are efficiently cached (for all users).', array('!performance-page' => \Drupal::url('system.performance_settings'), '!page-cache' => (\Drupal::moduleHandler()->moduleExists('page_cache')) ? \Drupal::url('help.page', array('name' => 'page_cache')) : '#', '!smart-cache' => (\Drupal::moduleHandler()->moduleExists('smart_cache')) ? \Drupal::url('help.page', array('name' => 'smart_cache')) : '#')) . '
'; $output .= '
' . t('Configuring cron') . '
'; $output .= '
' . t('In order for the site and its modules to continue to operate well, a set of routine administrative operations must run on a regular basis; these operations are known as cron tasks. On the Cron page, you can configure cron to run periodically as part of normal page requests, or you can turn this off and trigger cron from an outside process on your web server. You can verify the status of cron tasks by visiting the Status report page. For more information, see the online documentation for configuring cron jobs.', array('!status' => \Drupal::url('system.status'), '!handbook' => 'https://www.drupal.org/cron', '!cron' => \Drupal::url('system.cron_settings'))) . '
'; $output .= '
' . t('Configuring the file system') . '
'; diff --git a/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php b/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php index 1e8e2f4..327a8d5 100644 --- a/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php +++ b/core/modules/system/tests/modules/paramconverter_test/src/TestControllers.php @@ -25,6 +25,8 @@ public function testNodeSetParent(NodeInterface $node, NodeInterface $parent) { } public function testEntityLanguage(NodeInterface $node) { - return ['#markup' => $node->label()]; + $build = ['#markup' => $node->label()]; + \Drupal::service('renderer')->addCacheableDependency($build, $node); + return $build; } } diff --git a/core/profiles/standard/src/Tests/StandardTest.php b/core/profiles/standard/src/Tests/StandardTest.php index 2a7f4ef..0c977fc 100644 --- a/core/profiles/standard/src/Tests/StandardTest.php +++ b/core/profiles/standard/src/Tests/StandardTest.php @@ -9,6 +9,8 @@ use Drupal\config\Tests\SchemaCheckTestTrait; use Drupal\contact\Entity\ContactForm; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Url; use Drupal\filter\Entity\FilterFormat; use Drupal\simpletest\WebTestBase; use Drupal\user\Entity\Role; @@ -177,5 +179,34 @@ function testStandard() { $this->assertText('Max 650x650'); $this->assertText('Max 1300x1300'); $this->assertText('Max 2600x2600'); + + // Verify certain routes' responses are cacheable by SmartCache, to ensure + // these responses are very fast for authenticated users. + $url = Url::fromRoute('contact.site_page'); + $this->drupalGet($url); + $this->assertEqual('UNCACHEABLE', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Site-wide contact page cannot be cached by SmartCache.'); + + $url = Url::fromRoute(''); + $this->drupalGet($url); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Frontpage is cached by SmartCache.'); + + $url = Url::fromRoute('entity.node.canonical', ['node' => 1]); + $this->drupalGet($url); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'Full node page is cached by SmartCache.'); + + $url = Url::fromRoute('entity.user.canonical', ['user' => 1]); + $this->drupalGet($url); + $this->drupalGet($url); + $this->assertEqual('HIT', $this->drupalGetHeader('X-Drupal-SmartCache'), 'User profile page is cached by SmartCache.'); + + $url = Url::fromRoute('system.admin'); + $this->drupalGet($url); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'Admin pages cannot be cached by SmartCache.'); + + $url = Url::fromRoute('system.db_update'); + $this->drupalGet($url); + $this->assertFalse($this->drupalGetHeader('X-Drupal-SmartCache'), 'update.php page cannot be cached by SmartCache.'); } } diff --git a/core/profiles/testing/testing.info.yml b/core/profiles/testing/testing.info.yml index 5ded376..8b04df4 100644 --- a/core/profiles/testing/testing.info.yml +++ b/core/profiles/testing/testing.info.yml @@ -5,9 +5,10 @@ version: VERSION core: 8.x hidden: true dependencies: - # Enable page_cache in testing, to ensure that as many tests as possible run - # with page caching enabled. + # Enable page_cache and smart_cache in testing, to ensure that as many tests + # as possible run with anonymous page caching and SmartCache enabled. - page_cache + - smart_cache # @todo: Remove this in https://www.drupal.org/node/2352949 themes: - classy diff --git a/sites/example.settings.local.php b/sites/example.settings.local.php index 23090a4..32b4f9a 100644 --- a/sites/example.settings.local.php +++ b/sites/example.settings.local.php @@ -52,12 +52,25 @@ /** * Disable the render cache (this includes the page cache). * + * Note: you should test with the render cache enabled, to ensure the correct + * cacheability metadata is present (and hence the expected behavior). However, + * in the early stages of development, you may want to disable it. + * * This setting disables the render cache by using the Null cache back-end * defined by the development.services.yml file above. * * Do not use this setting until after the site is installed. */ -$settings['cache']['bins']['render'] = 'cache.backend.null'; +# $settings['cache']['bins']['render'] = 'cache.backend.null'; + +/** + * Disable SmartCache. + * + * Note: you should test with SmartCache enabled, to ensure the correct + * cacheability metadata is present (and hence the expected behavior). However, + * in the early stages of development, you may want to disable it. + */ +# $settings['cache']['bins']['smart_cache'] = 'cache.backend.null'; /** * Allow test modules and themes to be installed.