diff options
author | Alex Pott | 2014-09-20 10:14:29 (GMT) |
---|---|---|
committer | Alex Pott | 2014-09-20 10:14:29 (GMT) |
commit | fb6c562c9eef10adf986959dbeca1e7bc6d490a9 (patch) | |
tree | 71eb2d6e0c1264ba8eef6dd2f6a2e5fa1af56bd7 /core | |
parent | 1476c56c62e4d84ef3e9a57029a92b1f0773cbc8 (diff) |
Issue #2263981 by znerol, beejeebus: Introduce a robust and extensible page cache-policy framework.
Diffstat (limited to 'core')
33 files changed, 1314 insertions, 78 deletions
diff --git a/core/core.services.yml b/core/core.services.yml index 0f4d586..173f608 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -100,6 +100,18 @@ services: factory_method: get factory_service: cache_factory arguments: [discovery] + page_cache_request_policy: + class: Drupal\Core\PageCache\DefaultRequestPolicy + tags: + - { name: service_collector, tag: page_cache_request_policy, call: addPolicy} + page_cache_response_policy: + class: Drupal\Core\PageCache\ChainResponsePolicy + tags: + - { name: service_collector, tag: page_cache_response_policy, call: addPolicy} + page_cache_kill_switch: + class: Drupal\Core\PageCache\ResponsePolicy\KillSwitch + tags: + - { name: page_cache_response_policy } config.manager: class: Drupal\Core\Config\ConfigManager arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage', '@event_dispatcher'] @@ -729,7 +741,7 @@ services: class: Drupal\Core\EventSubscriber\FinishResponseSubscriber tags: - { name: event_subscriber } - arguments: ['@language_manager', '@config.factory'] + arguments: ['@language_manager', '@config.factory', '@page_cache_request_policy', '@page_cache_response_policy'] redirect_response_subscriber: class: Drupal\Core\EventSubscriber\RedirectResponseSubscriber arguments: ['@url_generator'] diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 1e1cc30..553811f 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -390,30 +390,6 @@ function drupal_page_get_cache(Request $request) { } /** - * Determines the cacheability of the current page. - * - * Note: we do not serve cached pages to authenticated users, or to anonymous - * users when $_SESSION is non-empty. $_SESSION may contain status messages - * from a form submission, the contents of a shopping cart, or other user- - * specific content that should not be cached and displayed to other users. - * - * @param $allow_caching - * Set to FALSE if you want to prevent this page to get cached. - * - * @return - * TRUE if the current page can be cached, FALSE otherwise. - */ -function drupal_page_is_cacheable($allow_caching = NULL) { - $allow_caching_static = &drupal_static(__FUNCTION__, TRUE); - if (isset($allow_caching)) { - $allow_caching_static = $allow_caching; - } - - return $allow_caching_static && ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD') - && PHP_SAPI !== 'cli'; -} - -/** * Sets an HTTP response header for the current page. * * Note: When sending a Content-Type header, always include a 'charset' type, @@ -931,7 +907,7 @@ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE) } // Mark this page as being uncacheable. - drupal_page_is_cacheable(FALSE); + \Drupal::service('page_cache_kill_switch')->trigger(); } // Messages not set when DB connection fails. diff --git a/core/includes/utility.inc b/core/includes/utility.inc index 3192deb..ec6a20c 100644 --- a/core/includes/utility.inc +++ b/core/includes/utility.inc @@ -62,8 +62,8 @@ function drupal_rebuild(ClassLoader $classloader, Request $request) { $bin->deleteAll(); } - // Disable the page cache. - drupal_page_is_cacheable(FALSE); + // Disable recording of cached pages. + \Drupal::service('page_cache_kill_switch')->trigger(); drupal_flush_all_caches(); diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 6f7b011..065afae 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -19,6 +19,7 @@ use Drupal\Core\DependencyInjection\ServiceProviderInterface; use Drupal\Core\DependencyInjection\YamlFileLoader; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Language\Language; +use Drupal\Core\PageCache\RequestPolicyInterface; use Drupal\Core\PhpStorage\PhpStorageFactory; use Drupal\Core\Site\Settings; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -466,9 +467,8 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { $cache_enabled = $config->get('cache.page.use_internal'); } - // If there is no session cookie and cache is enabled (or forced), try to - // serve a cached page. - if (!$request->cookies->has(session_name()) && $cache_enabled && drupal_page_is_cacheable()) { + $request_policy = \Drupal::service('page_cache_request_policy'); + if ($cache_enabled && $request_policy->check($request) === RequestPolicyInterface::ALLOW) { // Get the page from the cache. $response = drupal_page_get_cache($request); // If there is a cached page, display it. diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php index f882c29..c5a7be1 100644 --- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php @@ -11,6 +11,8 @@ use Drupal\Component\Datetime\DateTimePlus; use Drupal\Core\Config\Config; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\PageCache\RequestPolicyInterface; +use Drupal\Core\PageCache\ResponsePolicyInterface; use Drupal\Core\Site\Settings; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\Request; @@ -41,16 +43,36 @@ class FinishResponseSubscriber implements EventSubscriberInterface { protected $config; /** + * A policy rule determining the cacheability of a request. + * + * @var \Drupal\Core\PageCache\RequestPolicyInterface + */ + protected $requestPolicy; + + /** + * A policy rule determining the cacheability of the response. + * + * @var \Drupal\Core\PageCache\ResponsePolicyInterface + */ + protected $responsePolicy; + + /** * Constructs a FinishResponseSubscriber object. * * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * The language manager object for retrieving the correct language code. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * A config factory for retrieving required config objects. + * @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 a response. */ - public function __construct(LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory) { + public function __construct(LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy) { $this->languageManager = $language_manager; $this->config = $config_factory->get('system.performance'); + $this->requestPolicy = $request_policy; + $this->responsePolicy = $response_policy; } /** @@ -83,16 +105,21 @@ class FinishResponseSubscriber implements EventSubscriberInterface { $response->headers->set($name, $value, FALSE); } - $is_cacheable = drupal_page_is_cacheable(); + $is_cacheable = ($this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) && ($this->responsePolicy->check($response, $request) !== ResponsePolicyInterface::DENY); // Add headers necessary to specify whether the response should be cached by // proxies and/or the browser. if ($is_cacheable && $this->config->get('cache.page.max_age') > 0) { if (!$this->isCacheControlCustomized($response)) { + // Only add the default Cache-Control header if the controller did not + // specify one on the response. $this->setResponseCacheable($response, $request); } } else { + // If either the policy forbids caching or the sites configuration does + // not allow to add a max-age directive, then enforce a Cache-Control + // header declaring the response as not cacheable. $this->setResponseNotCacheable($response, $request); } diff --git a/core/lib/Drupal/Core/PageCache/ChainRequestPolicy.php b/core/lib/Drupal/Core/PageCache/ChainRequestPolicy.php new file mode 100644 index 0000000..23dc1d6 --- /dev/null +++ b/core/lib/Drupal/Core/PageCache/ChainRequestPolicy.php @@ -0,0 +1,65 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\PageCache\ChainRequestPolicy. + */ + +namespace Drupal\Core\PageCache; + +use Symfony\Component\HttpFoundation\Request; + +/** + * Implements a compound request policy. + * + * When evaluating the compound policy, all of the contained rules are applied + * to the request. The overall result is computed according to the following + * rules: + * + * <ol> + * <li>Returns static::DENY if any of the rules evaluated to static::DENY</li> + * <li>Returns static::ALLOW if at least one of the rules evaluated to + * static::ALLOW and none to static::DENY</li> + * <li>Otherwise returns NULL</li> + * </ol> + */ +class ChainRequestPolicy implements ChainRequestPolicyInterface { + + /** + * A list of policy rules to apply when this policy is evaluated. + * + * @var \Drupal\Core\PageCache\RequestPolicyInterface[] + */ + protected $rules = []; + + /** + * {@inheritdoc} + */ + public function check(Request $request) { + $final_result = NULL; + + foreach ($this->rules as $rule) { + $result = $rule->check($request); + if ($result === static::DENY) { + return $result; + } + elseif ($result === static::ALLOW) { + $final_result = $result; + } + elseif (isset($result)) { + throw new \UnexpectedValueException('Return value of RequestPolicyInterface::check() must be one of RequestPolicyInterface::ALLOW, RequestPolicyInterface::DENY or NULL'); + } + } + + return $final_result; + } + + /** + * {@inheritdoc} + */ + public function addPolicy(RequestPolicyInterface $policy) { + $this->rules[] = $policy; + return $this; + } + +} diff --git a/core/lib/Drupal/Core/PageCache/ChainRequestPolicyInterface.php b/core/lib/Drupal/Core/PageCache/ChainRequestPolicyInterface.php new file mode 100644 index 0000000..e8548df --- /dev/null +++ b/core/lib/Drupal/Core/PageCache/ChainRequestPolicyInterface.php @@ -0,0 +1,25 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\PageCache\ChainRequestPolicyInterface. + */ + +namespace Drupal\Core\PageCache; + +/** + * Defines the interface for compound request policies. + */ +interface ChainRequestPolicyInterface extends RequestPolicyInterface { + + /** + * Add a policy to the list of policy rules. + * + * @param \Drupal\Core\PageCache\RequestPolicyInterface $policy + * The request policy rule to add. + * + * @return $this + */ + public function addPolicy(RequestPolicyInterface $policy); + +} diff --git a/core/lib/Drupal/Core/PageCache/ChainResponsePolicy.php b/core/lib/Drupal/Core/PageCache/ChainResponsePolicy.php new file mode 100644 index 0000000..20d5e39 --- /dev/null +++ b/core/lib/Drupal/Core/PageCache/ChainResponsePolicy.php @@ -0,0 +1,56 @@ +<?php +/** + * @file + * Contains \Drupal\Core\PageCache\ChainResponsePolicy. + */ + +namespace Drupal\Core\PageCache; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Implements a compound response policy. + * + * When evaluating the compound policy, all of the contained rules are applied + * to the response. The overall result is computed according to the following + * rules: + * + * <ol> + * <li>Returns static::DENY if any of the rules evaluated to static::DENY</li> + * <li>Otherwise returns NULL</li> + * </ol> + */ +class ChainResponsePolicy implements ChainResponsePolicyInterface { + + /** + * A list of policy rules to apply when this policy is checked. + * + * @var \Drupal\Core\PageCache\ResponsePolicyInterface[] + */ + protected $rules = []; + + /** + * {@inheritdoc} + */ + public function check(Response $response, Request $request) { + foreach ($this->rules as $rule) { + $result = $rule->check($response, $request); + if ($result === static::DENY) { + return $result; + } + elseif (isset($result)) { + throw new \UnexpectedValueException('Return value of ResponsePolicyInterface::check() must be one of ResponsePolicyInterface::DENY or NULL'); + } + } + } + + /** + * {@inheritdoc} + */ + public function addPolicy(ResponsePolicyInterface $policy) { + $this->rules[] = $policy; + return $this; + } + +} diff --git a/core/lib/Drupal/Core/PageCache/ChainResponsePolicyInterface.php b/core/lib/Drupal/Core/PageCache/ChainResponsePolicyInterface.php new file mode 100644 index 0000000..5018f19 --- /dev/null +++ b/core/lib/Drupal/Core/PageCache/ChainResponsePolicyInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * @file + * Contains \Drupal\Core\PageCache\ChainResponsePolicyInterface. + */ + +namespace Drupal\Core\PageCache; + +/** + * Defines the interface for compound request policies. + */ +interface ChainResponsePolicyInterface extends ResponsePolicyInterface { + + /** + * Add a policy to the list of policy rules. + * + * @param \Drupal\Core\PageCache\ResponsePolicyInterface $policy + * The request policy rule to add. + * + * @return $this + */ + public function addPolicy(ResponsePolicyInterface $policy); + +} diff --git a/core/lib/Drupal/Core/PageCache/DefaultRequestPolicy.php b/core/lib/Drupal/Core/PageCache/DefaultRequestPolicy.php new file mode 100644 index 0000000..b327753 --- /dev/null +++ b/core/lib/Drupal/Core/PageCache/DefaultRequestPolicy.php @@ -0,0 +1,27 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\PageCache\DefaultRequestPolicy. + */ + +namespace Drupal\Core\PageCache; + +/** + * The default page cache request policy. + * + * Delivery of cached pages is denied if either the application is running from + * the command line or the request was not initiated with a safe method (GET or + * HEAD). Also caching is only allowed for requests without a session cookie. + */ +class DefaultRequestPolicy extends ChainRequestPolicy { + + /** + * Constructs the default page cache request policy. + */ + public function __construct() { + $this->addPolicy(new RequestPolicy\CommandLineOrUnsafeMethod()); + $this->addPolicy(new RequestPolicy\NoSessionOpen()); + } + +} diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicy/CommandLineOrUnsafeMethod.php b/core/lib/Drupal/Core/PageCache/RequestPolicy/CommandLineOrUnsafeMethod.php new file mode 100644 index 0000000..66a3bf6 --- /dev/null +++ b/core/lib/Drupal/Core/PageCache/RequestPolicy/CommandLineOrUnsafeMethod.php @@ -0,0 +1,38 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\PageCache\RequestPolicy\CommandLineOrUnsafeMethod. + */ + +namespace Drupal\Core\PageCache\RequestPolicy; + +use Drupal\Core\PageCache\RequestPolicyInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * Reject when running from the command line or when HTTP method is not safe. + * + * The policy denies caching if the request was initiated from the command line + * interface (drush) or the request method is neither GET nor HEAD (see RFC + * 2616, section 9.1.1 - Safe Methods). + */ +class CommandLineOrUnsafeMethod implements RequestPolicyInterface { + + /** + * {@inheritdoc} + */ + public function check(Request $request) { + if ($this->isCli() || !$request->isMethodSafe()) { + return static::DENY; + } + } + + /** + * Indicates whether this is a CLI request. + */ + protected function isCli() { + return PHP_SAPI === 'cli'; + } + +} diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicy/NoSessionOpen.php b/core/lib/Drupal/Core/PageCache/RequestPolicy/NoSessionOpen.php new file mode 100644 index 0000000..c18cf56 --- /dev/null +++ b/core/lib/Drupal/Core/PageCache/RequestPolicy/NoSessionOpen.php @@ -0,0 +1,49 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\PageCache\RequestPolicy\NoSessionOpen. + */ + +namespace Drupal\Core\PageCache\RequestPolicy; + +use Drupal\Core\PageCache\RequestPolicyInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * A policy allowing delivery of cached pages when there is no session open. + * + * Do not serve cached pages to authenticated users, or to anonymous users when + * $_SESSION is non-empty. $_SESSION may contain status messages from a form + * submission, the contents of a shopping cart, or other userspecific content + * that should not be cached and displayed to other users. + */ +class NoSessionOpen implements RequestPolicyInterface { + + /** + * The name of the session cookie. + * + * @var string + */ + protected $sessionCookieName; + + /** + * Constructs a new page cache session policy. + * + * @param string $session_cookie_name + * (optional) The name of the session cookie. Defaults to session_name(). + */ + public function __construct($session_cookie_name = NULL) { + $this->sessionCookieName = $session_cookie_name ?: session_name(); + } + + /** + * {@inheritdoc} + */ + public function check(Request $request) { + if (!$request->cookies->has($this->sessionCookieName)) { + return static::ALLOW; + } + } + +} diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicyInterface.php b/core/lib/Drupal/Core/PageCache/RequestPolicyInterface.php new file mode 100644 index 0000000..3a4ef21 --- /dev/null +++ b/core/lib/Drupal/Core/PageCache/RequestPolicyInterface.php @@ -0,0 +1,54 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\PageCache\RequestPolicyInterface. + */ + +namespace Drupal\Core\PageCache; + +use Symfony\Component\HttpFoundation\Request; + +/** + * Defines the interface for request policy implementations. + * + * The request policy is evaluated in order to determine whether delivery of a + * cached page should be attempted. The caller should do so if static::ALLOW is + * returned from the check() method. + */ +interface RequestPolicyInterface { + + /** + * Allow delivery of cached pages. + */ + const ALLOW = 'allow'; + + /** + * Deny delivery of cached pages. + */ + const DENY = 'deny'; + + /** + * Determines whether delivery of a cached page should be attempted. + * + * Note that the request-policy check runs very early. In particular it is + * not possible to determine the logged in user. Also the current route match + * is not yet present when the check runs. Therefore, request-policy checks + * need to be designed in a way such that they do not depend on any other + * service and only take in account the information present on the incoming + * request. + * + * When matching against the request path, special attention is needed to + * support path prefixes which are often used on multilingual sites. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The incoming request object. + * + * @return string|NULL + * One of static::ALLOW, static::DENY or NULL. Calling code may attempt to + * deliver a cached page if static::ALLOW is returned. Returns NULL if the + * policy is not specified for the given request. + */ + public function check(Request $request); + +} diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicy/KillSwitch.php b/core/lib/Drupal/Core/PageCache/ResponsePolicy/KillSwitch.php new file mode 100644 index 0000000..62ecc45 --- /dev/null +++ b/core/lib/Drupal/Core/PageCache/ResponsePolicy/KillSwitch.php @@ -0,0 +1,42 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\PageCache\ResponsePolicy\KillSwitch. + */ + +namespace Drupal\Core\PageCache\ResponsePolicy; + +use Drupal\Core\PageCache\ResponsePolicyInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * A policy evaluating to static::DENY when the kill switch was triggered. + */ +class KillSwitch implements ResponsePolicyInterface { + + /** + * A flag indicating whether the kill switch was triggered. + * + * @var bool + */ + protected $kill = FALSE; + + /** + * {@inheritdoc} + */ + public function check(Response $response, Request $request) { + if ($this->kill) { + return static::DENY; + } + } + + /** + * Deny any page caching on the current request. + */ + public function trigger() { + $this->kill = TRUE; + } + +} diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicyInterface.php b/core/lib/Drupal/Core/PageCache/ResponsePolicyInterface.php new file mode 100644 index 0000000..596f43f --- /dev/null +++ b/core/lib/Drupal/Core/PageCache/ResponsePolicyInterface.php @@ -0,0 +1,42 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\PageCache\ResponsePolicyInterface. + */ + +namespace Drupal\Core\PageCache; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Defines the interface for response policy implementations. + * + * The response policy is evaluated in order to determine whether a page should + * be stored a in the cache. Calling code should do so unless static::DENY is + * returned from the check() method. + */ +interface ResponsePolicyInterface { + + /** + * Deny storage of a page in the cache. + */ + const DENY = 'deny'; + + /** + * Determines whether it is save to store a page in the cache. + * + * @param \Symfony\Component\HttpFoundation\Response $response + * The response which is about to be sent to the client. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * + * @return string|NULL + * Either static::DENY or NULL. Calling code may attempt to store a page in + * the cache unless static::DENY is returned. Returns NULL if the policy + * policy is not specified for the given response. + */ + public function check(Response $response, Request $request); + +} diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php index 64b8fdd..efb3b7a 100644 --- a/core/lib/Drupal/Core/Session/SessionManager.php +++ b/core/lib/Drupal/Core/Session/SessionManager.php @@ -130,9 +130,6 @@ class SessionManager extends NativeSessionStorage implements SessionManagerInter // anonymous users not use a session cookie unless something is stored in // $_SESSION. This allows HTTP proxies to cache anonymous pageviews. $result = $this->startNow(); - if ($user->isAuthenticated() || !$this->isSessionObsolete()) { - drupal_page_is_cacheable(FALSE); - } } if (empty($result)) { diff --git a/core/modules/image/image.services.yml b/core/modules/image/image.services.yml index 1447e03..7f1f985 100644 --- a/core/modules/image/image.services.yml +++ b/core/modules/image/image.services.yml @@ -6,3 +6,8 @@ services: plugin.manager.image.effect: class: Drupal\image\ImageEffectManager parent: default_plugin_manager + image.page_cache_request_policy.deny_private_image_style_download: + class: Drupal\image\PageCache\DenyPrivateImageStyleDownload + arguments: ['@current_route_match'] + tags: + - { name: page_cache_response_policy } diff --git a/core/modules/image/src/Controller/ImageStyleDownloadController.php b/core/modules/image/src/Controller/ImageStyleDownloadController.php index 3e4bcf0..a18c677 100644 --- a/core/modules/image/src/Controller/ImageStyleDownloadController.php +++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php @@ -155,7 +155,6 @@ class ImageStyleDownloadController extends FileDownloadController { } if ($success) { - drupal_page_is_cacheable(FALSE); $image = $this->imageFactory->get($derivative_uri); $uri = $image->getSource(); $headers += array( diff --git a/core/modules/image/src/PageCache/DenyPrivateImageStyleDownload.php b/core/modules/image/src/PageCache/DenyPrivateImageStyleDownload.php new file mode 100644 index 0000000..02cb27c --- /dev/null +++ b/core/modules/image/src/PageCache/DenyPrivateImageStyleDownload.php @@ -0,0 +1,49 @@ +<?php + +/** + * @file + * Contains \Drupal\image\PageCache\DenyPrivateImageStyleDownload. + */ + +namespace Drupal\image\PageCache; + +use Drupal\Core\PageCache\ResponsePolicyInterface; +use Drupal\Core\Routing\RouteMatchInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Cache policy for image preview page. + * + * This policy rule denies caching of responses generated by the + * entity.image.preview route. + */ +class DenyPrivateImageStyleDownload implements ResponsePolicyInterface { + + /** + * The current route match. + * + * @var \Drupal\Core\Routing\RouteMatchInterface + */ + protected $routeMatch; + + /** + * Constructs a deny image preview page cache policy. + * + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The current route match. + */ + public function __construct(RouteMatchInterface $route_match) { + $this->routeMatch = $route_match; + } + + /** + * {@inheritdoc} + */ + public function check(Response $response, Request $request) { + if ($this->routeMatch->getRouteName() === 'image.style_private') { + return static::DENY; + } + } + +} diff --git a/core/modules/image/tests/src/Unit/PageCache/DenyPrivateImageStyleDownloadTest.php b/core/modules/image/tests/src/Unit/PageCache/DenyPrivateImageStyleDownloadTest.php new file mode 100644 index 0000000..fab54d3 --- /dev/null +++ b/core/modules/image/tests/src/Unit/PageCache/DenyPrivateImageStyleDownloadTest.php @@ -0,0 +1,90 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\image\Unit\PageCache\DenyPrivateImageStyleDownloadTest. + */ + +namespace Drupal\Tests\image\Unit\PageCache; + +use Drupal\Core\PageCache\ResponsePolicyInterface; +use Drupal\image\PageCache\DenyPrivateImageStyleDownload; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * @coversDefaultClass \Drupal\image\PageCache\DenyPrivateImageStyleDownload + * @group image + */ +class DenyPrivateImageStyleDownloadTest extends UnitTestCase { + + /** + * The response policy under test. + * + * @var \Drupal\image\PageCache\DenyPrivateImageStyleDownload + */ + protected $policy; + + /** + * A request object. + * + * @var \Symfony\Component\HttpFoundation\Request + */ + protected $request; + + /** + * A response object. + * + * @var \Symfony\Component\HttpFoundation\Response + */ + protected $response; + + /** + * The current route match. + * + * @var \Drupal\Core\Routing\RouteMatch|\PHPUnit_Framework_MockObject_MockObject + */ + protected $routeMatch; + + public function setUp() { + $this->routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface'); + $this->policy = new DenyPrivateImageStyleDownload($this->routeMatch); + $this->response = new Response(); + $this->request = new Request(); + } + + /** + * Asserts that caching is denied on the private image style download route. + * + * @dataProvider providerPrivateImageStyleDownloadPolicy + * @covers ::check + */ + public function testPrivateImageStyleDownloadPolicy($expected_result, $route_name) { + $this->routeMatch->expects($this->once()) + ->method('getRouteName') + ->will($this->returnValue($route_name)); + + $actual_result = $this->policy->check($this->response, $this->request); + $this->assertSame($expected_result, $actual_result); + } + + /** + * Provides data and expected results for the test method. + * + * @return array + * Data and expected results. + */ + public function providerPrivateImageStyleDownloadPolicy() { + return [ + [ResponsePolicyInterface::DENY, 'image.style_private'], + [NULL, 'some.other.route'], + [NULL, NULL], + [NULL, FALSE], + [NULL, TRUE], + [NULL, new \StdClass()], + [NULL, [1, 2, 3]], + ]; + } + +} diff --git a/core/modules/node/node.services.yml b/core/modules/node/node.services.yml index 546f47f..68f0af1 100644 --- a/core/modules/node/node.services.yml +++ b/core/modules/node/node.services.yml @@ -34,3 +34,8 @@ services: arguments: ['@user.tempstore'] tags: - { name: paramconverter } + node.page_cache_request_policy.deny_node_preview: + class: Drupal\node\PageCache\DenyNodePreview + arguments: ['@current_route_match'] + tags: + - { name: page_cache_response_policy } diff --git a/core/modules/node/src/Controller/NodePreviewController.php b/core/modules/node/src/Controller/NodePreviewController.php index 9fb8d13..35c65d9 100644 --- a/core/modules/node/src/Controller/NodePreviewController.php +++ b/core/modules/node/src/Controller/NodePreviewController.php @@ -20,9 +20,6 @@ class NodePreviewController extends EntityViewController { * {@inheritdoc} */ public function view(EntityInterface $node_preview, $view_mode_id = 'full', $langcode = NULL) { - // Do not cache this page. - drupal_page_is_cacheable(FALSE); - $node_preview->preview_view_mode = $view_mode_id; $build = array('nodes' => parent::view($node_preview, $view_mode_id)); diff --git a/core/modules/node/src/PageCache/DenyNodePreview.php b/core/modules/node/src/PageCache/DenyNodePreview.php new file mode 100644 index 0000000..5e749a0 --- /dev/null +++ b/core/modules/node/src/PageCache/DenyNodePreview.php @@ -0,0 +1,49 @@ +<?php + +/** + * @file + * Contains \Drupal\node\PageCache\DenyNodePreview. + */ + +namespace Drupal\node\PageCache; + +use Drupal\Core\PageCache\ResponsePolicyInterface; +use Drupal\Core\Routing\RouteMatchInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Cache policy for node preview page. + * + * This policy rule denies caching of responses generated by the + * entity.node.preview route. + */ +class DenyNodePreview implements ResponsePolicyInterface { + + /** + * The current route match. + * + * @var \Drupal\Core\Routing\RouteMatchInterface + */ + protected $routeMatch; + + /** + * Constructs a deny node preview page cache policy. + * + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The current route match. + */ + public function __construct(RouteMatchInterface $route_match) { + $this->routeMatch = $route_match; + } + + /** + * {@inheritdoc} + */ + public function check(Response $response, Request $request) { + if ($this->routeMatch->getRouteName() === 'entity.node.preview') { + return static::DENY; + } + } + +} diff --git a/core/modules/node/tests/src/Unit/PageCache/DenyNodePreviewTest.php b/core/modules/node/tests/src/Unit/PageCache/DenyNodePreviewTest.php new file mode 100644 index 0000000..ea90c53 --- /dev/null +++ b/core/modules/node/tests/src/Unit/PageCache/DenyNodePreviewTest.php @@ -0,0 +1,90 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\node\Unit\PageCache\DenyNodePreviewTest. + */ + +namespace Drupal\Tests\node\Unit\PageCache; + +use Drupal\Core\PageCache\ResponsePolicyInterface; +use Drupal\node\PageCache\DenyNodePreview; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * @coversDefaultClass \Drupal\node\PageCache\DenyNodePreview + * @group node + */ +class DenyNodePreviewTest extends UnitTestCase { + + /** + * The response policy under test. + * + * @var \Drupal\node\PageCache\DenyNodePreview + */ + protected $policy; + + /** + * A request object. + * + * @var \Symfony\Component\HttpFoundation\Request + */ + protected $request; + + /** + * A response object. + * + * @var \Symfony\Component\HttpFoundation\Response + */ + protected $response; + + /** + * The current route match. + * + * @var \Drupal\Core\Routing\RouteMatch|\PHPUnit_Framework_MockObject_MockObject + */ + protected $routeMatch; + + public function setUp() { + $this->routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface'); + $this->policy = new DenyNodePreview($this->routeMatch); + $this->response = new Response(); + $this->request = new Request(); + } + + /** + * Asserts that caching is denied on the node preview route. + * + * @dataProvider providerPrivateImageStyleDownloadPolicy + * @covers ::check + */ + public function testPrivateImageStyleDownloadPolicy($expected_result, $route_name) { + $this->routeMatch->expects($this->once()) + ->method('getRouteName') + ->will($this->returnValue($route_name)); + + $actual_result = $this->policy->check($this->response, $this->request); + $this->assertSame($expected_result, $actual_result); + } + + /** + * Provides data and expected results for the test method. + * + * @return array + * Data and expected results. + */ + public function providerPrivateImageStyleDownloadPolicy() { + return [ + [ResponsePolicyInterface::DENY, 'entity.node.preview'], + [NULL, 'some.other.route'], + [NULL, NULL], + [NULL, FALSE], + [NULL, TRUE], + [NULL, new \StdClass()], + [NULL, [1, 2, 3]], + ]; + } + +} diff --git a/core/modules/toolbar/src/Controller/ToolbarController.php b/core/modules/toolbar/src/Controller/ToolbarController.php index 2663951..aba5357 100644 --- a/core/modules/toolbar/src/Controller/ToolbarController.php +++ b/core/modules/toolbar/src/Controller/ToolbarController.php @@ -22,10 +22,22 @@ class ToolbarController extends ControllerBase { * @return \Symfony\Component\HttpFoundation\JsonResponse */ public function subtreesJsonp() { - _toolbar_initialize_page_cache(); $subtrees = toolbar_get_rendered_subtrees(); $response = new JsonResponse($subtrees); $response->setCallback('Drupal.toolbar.setSubtrees.resolve'); + + // The Expires HTTP header is the heart of the client-side HTTP caching. The + // additional server-side page cache only takes effect when the client + // accesses the callback URL again (e.g., after clearing the browser cache + // or when force-reloading a Drupal page). + $max_age = 365 * 24 * 60 * 60; + $response->setPrivate(); + $response->setMaxAge($max_age); + + $expires = new \DateTime(); + $expires->setTimestamp(REQUEST_TIME + $max_age); + $response->setExpires($expires); + return $response; } diff --git a/core/modules/toolbar/src/PageCache/AllowToolbarPath.php b/core/modules/toolbar/src/PageCache/AllowToolbarPath.php new file mode 100644 index 0000000..81d5734 --- /dev/null +++ b/core/modules/toolbar/src/PageCache/AllowToolbarPath.php @@ -0,0 +1,32 @@ +<?php + +/** + * @file + * Contains \Drupal\toolbar\PageCache\AllowToolbarPath. + */ + +namespace Drupal\toolbar\PageCache; + +use Drupal\Core\PageCache\RequestPolicyInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * Cache policy for the toolbar page cache service. + * + * This policy allows caching of requests directed to /toolbar/subtrees/{hash} + * even for authenticated users. + */ +class AllowToolbarPath implements RequestPolicyInterface { + + /** + * {@inheritdoc} + */ + public function check(Request $request) { + // Note that this regular expression matches the end of pathinfo in order to + // support multilingual sites using path prefixes. + if (preg_match('#/toolbar/subtrees/[^/]+(/[^/]+)?$#', $request->getPathInfo())) { + return static::ALLOW; + } + } + +} diff --git a/core/modules/toolbar/tests/src/Unit/PageCache/AllowToolbarPathTest.php b/core/modules/toolbar/tests/src/Unit/PageCache/AllowToolbarPathTest.php new file mode 100644 index 0000000..f1223be --- /dev/null +++ b/core/modules/toolbar/tests/src/Unit/PageCache/AllowToolbarPathTest.php @@ -0,0 +1,65 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\toolbar\Unit\PageCache\AllowToolbarPathTest. + */ + +namespace Drupal\Tests\toolbar\Unit\PageCache; + +use Drupal\toolbar\PageCache\AllowToolbarPath; +use Drupal\Core\PageCache\RequestPolicyInterface; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\HttpFoundation\Request; + +/** + * @coversDefaultClass \Drupal\toolbar\PageCache\AllowToolbarPath + * @group toolbar + */ +class AllowToolbarPathTest extends UnitTestCase { + + /** + * The toolbar path policy under test. + * + * @var \Drupal\toolbar\PageCache\AllowToolbarPath + */ + protected $policy; + + public function setUp() { + $this->policy = new AllowToolbarPath(); + } + + /** + * Asserts that caching is allowed if the request goes to toolbar subtree. + * + * @dataProvider providerTestAllowToolbarPath + * @covers ::check + */ + public function testAllowToolbarPath($expected_result, $path) { + $request = Request::create($path); + $result = $this->policy->check($request); + $this->assertSame($expected_result, $result); + } + + /** + * Provides data and expected results for the test method. + * + * @return array + * Data and expected results. + */ + public function providerTestAllowToolbarPath() { + return [ + [NULL, '/'], + [NULL, '/other-path?q=/toolbar/subtrees/'], + [NULL, '/toolbar/subtrees/'], + [NULL, '/toolbar/subtrees/some-hash/langcode/additional-stuff'], + [RequestPolicyInterface::ALLOW, '/de/toolbar/subtrees/abcd'], + [RequestPolicyInterface::ALLOW, '/en-us/toolbar/subtrees/xyz'], + [RequestPolicyInterface::ALLOW, '/en-us/toolbar/subtrees/xyz/de'], + [RequestPolicyInterface::ALLOW, '/a/b/c/toolbar/subtrees/xyz/de'], + [RequestPolicyInterface::ALLOW, '/toolbar/subtrees/some-hash'], + [RequestPolicyInterface::ALLOW, '/toolbar/subtrees/some-hash/en'], + ]; + } + +} diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index 07deb3c..9643981 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -96,43 +96,6 @@ function toolbar_element_info() { } /** - * Use Drupal's page cache for toolbar/subtrees/*, even for authenticated users. - * - * This gets invoked after full bootstrap, so must duplicate some of what's - * done by \Drupal\Core\DrupalKernel::handlePageCache(). - * - * @todo Replace this hack with something better integrated with DrupalKernel - * once Drupal's page caching itself is properly integrated. - */ -function _toolbar_initialize_page_cache() { - $GLOBALS['conf']['system.performance']['cache']['page']['enabled'] = TRUE; - drupal_page_is_cacheable(TRUE); - - // If we have a cache, serve it. - // @see \Drupal\Core\DrupalKernel::handlePageCache() - $request = \Drupal::request(); - $response = drupal_page_get_cache($request); - if ($response) { - $response->headers->set('X-Drupal-Cache', 'HIT'); - - drupal_serve_page_from_cache($response, $request); - - $response->prepare($request); - $response->send(); - // We are done. - exit; - } - - // The Expires HTTP header is the heart of the client-side HTTP caching. The - // additional server-side page cache only takes effect when the client - // accesses the callback URL again (e.g., after clearing the browser cache or - // when force-reloading a Drupal page). - $max_age = 3600 * 24 * 365; - drupal_add_http_header('Expires', gmdate(DateTimePlus::RFC7231, REQUEST_TIME + $max_age)); - drupal_add_http_header('Cache-Control', 'private, max-age=' . $max_age); -} - -/** * Implements hook_page_build(). * * Add admin toolbar to the page_top region automatically. diff --git a/core/modules/toolbar/toolbar.services.yml b/core/modules/toolbar/toolbar.services.yml index 7f26968..278d3c7 100644 --- a/core/modules/toolbar/toolbar.services.yml +++ b/core/modules/toolbar/toolbar.services.yml @@ -6,3 +6,7 @@ services: factory_method: get factory_service: cache_factory arguments: [toolbar] + toolbar.page_cache_request_policy.allow_toolbar_path: + class: Drupal\toolbar\PageCache\AllowToolbarPath + tags: + - { name: page_cache_request_policy } diff --git a/core/tests/Drupal/Tests/Core/PageCache/ChainRequestPolicyTest.php b/core/tests/Drupal/Tests/Core/PageCache/ChainRequestPolicyTest.php new file mode 100644 index 0000000..3343735 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/PageCache/ChainRequestPolicyTest.php @@ -0,0 +1,165 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\PageCache\ChainRequestPolicyTest. + */ + +namespace Drupal\Tests\Core\PageCache; + +use Drupal\Core\PageCache\RequestPolicyInterface; +use Drupal\Core\PageCache\ChainRequestPolicy; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\HttpFoundation\Request; + +/** + * @coversDefaultClass \Drupal\Core\PageCache\ChainRequestPolicy + * @group PageCache + */ +class ChainRequestPolicyTest extends UnitTestCase { + + /** + * The chain request policy under test. + * + * @var \Drupal\Core\PageCache\ChainRequestPolicy + */ + protected $policy; + + /** + * A request object. + * + * @var \Symfony\Component\HttpFoundation\Request + */ + protected $request; + + public function setUp() { + $this->policy = new ChainRequestPolicy(); + $this->request = new Request(); + } + + /** + * Asserts that check() returns NULL if the chain is empty. + * + * @covers ::check + */ + public function testEmptyChain() { + $result = $this->policy->check($this->request); + $this->assertSame(NULL, $result); + } + + /** + * Asserts that check() returns NULL if a rule returns NULL. + * + * @covers ::check + */ + public function testNullRuleChain() { + $rule = $this->getMock('Drupal\Core\PageCache\RequestPolicyInterface'); + $rule->expects($this->once()) + ->method('check') + ->with($this->request) + ->will($this->returnValue(NULL)); + + $this->policy->addPolicy($rule); + + $result = $this->policy->check($this->request); + $this->assertSame(NULL, $result); + } + + /** + * Asserts that check() throws an exception if a rule returns an invalid value. + * + * @expectedException \UnexpectedValueException + * @dataProvider providerChainExceptionOnInvalidReturnValue + * @covers ::check + */ + public function testChainExceptionOnInvalidReturnValue($return_value) { + $rule = $this->getMock('Drupal\Core\PageCache\RequestPolicyInterface'); + $rule->expects($this->once()) + ->method('check') + ->with($this->request) + ->will($this->returnValue($return_value)); + + $this->policy->addPolicy($rule); + + $this->policy->check($this->request); + } + + /** + * Provides test data for testChainExceptionOnInvalidReturnValue. + * + * @return array + * Test input and expected result. + */ + public function providerChainExceptionOnInvalidReturnValue() { + return [ + [FALSE], + [0], + [1], + [TRUE], + [[1, 2, 3]], + [new \stdClass()], + ]; + } + + /** + * Asserts that check() returns ALLOW if any of the rules returns ALLOW. + * + * @dataProvider providerAllowIfAnyRuleReturnedAllow + * @covers ::check + */ + public function testAllowIfAnyRuleReturnedAllow($return_values) { + foreach ($return_values as $return_value) { + $rule = $this->getMock('Drupal\Core\PageCache\RequestPolicyInterface'); + $rule->expects($this->once()) + ->method('check') + ->with($this->request) + ->will($this->returnValue($return_value)); + + $this->policy->addPolicy($rule); + } + + $actual_result = $this->policy->check($this->request); + $this->assertSame(RequestPolicyInterface::ALLOW, $actual_result); + } + + /** + * Provides test data for testAllowIfAnyRuleReturnedAllow. + * + * @return array + * Test input and expected result. + */ + public function providerAllowIfAnyRuleReturnedAllow() { + return [ + [[RequestPolicyInterface::ALLOW]], + [[NULL, RequestPolicyInterface::ALLOW]], + ]; + } + + /** + * Asserts that check() returns immediately when a rule returned DENY. + */ + public function testStopChainOnFirstDeny() { + $rule1 = $this->getMock('Drupal\Core\PageCache\RequestPolicyInterface'); + $rule1->expects($this->once()) + ->method('check') + ->with($this->request) + ->will($this->returnValue(RequestPolicyInterface::ALLOW)); + $this->policy->addPolicy($rule1); + + $deny_rule = $this->getMock('Drupal\Core\PageCache\RequestPolicyInterface'); + $deny_rule->expects($this->once()) + ->method('check') + ->with($this->request) + ->will($this->returnValue(RequestPolicyInterface::DENY)); + $this->policy->addPolicy($deny_rule); + + $ignored_rule = $this->getMock('Drupal\Core\PageCache\RequestPolicyInterface'); + $ignored_rule->expects($this->never()) + ->method('check'); + $this->policy->addPolicy($ignored_rule); + + $actual_result = $this->policy->check($this->request); + $this->assertsame(RequestPolicyInterface::DENY, $actual_result); + } + +} diff --git a/core/tests/Drupal/Tests/Core/PageCache/ChainResponsePolicyTest.php b/core/tests/Drupal/Tests/Core/PageCache/ChainResponsePolicyTest.php new file mode 100644 index 0000000..d991678 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/PageCache/ChainResponsePolicyTest.php @@ -0,0 +1,140 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\PageCache\ChainResponsePolicyTest. + */ + +namespace Drupal\Tests\Core\PageCache; + +use Drupal\Core\PageCache\ResponsePolicyInterface; +use Drupal\Core\PageCache\ChainResponsePolicy; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * @coversDefaultClass \Drupal\Core\PageCache\ChainResponsePolicy + * @group PageCache + */ +class ChainResponsePolicyTest extends UnitTestCase { + + /** + * The chain response policy under test. + * + * @var \Drupal\Core\PageCache\ChainResponsePolicy + */ + protected $policy; + + /** + * A request object. + * + * @var \Symfony\Component\HttpFoundation\Request + */ + protected $request; + + /** + * A response object. + * + * @var \Symfony\Component\HttpFoundation\Response + */ + protected $response; + + public function setUp() { + $this->policy = new ChainResponsePolicy(); + $this->response = new Response(); + $this->request = new Request(); + } + + /** + * Asserts that check() returns NULL if the chain is empty. + * + * @covers ::check + */ + public function testEmptyChain() { + $result = $this->policy->check($this->response, $this->request); + $this->assertSame(NULL, $result); + } + + /** + * Asserts that check() returns NULL if a rule returns NULL. + * + * @covers ::check + */ + public function testNullRuleChain() { + $rule = $this->getMock('Drupal\Core\PageCache\ResponsePolicyInterface'); + $rule->expects($this->once()) + ->method('check') + ->with($this->response, $this->request) + ->will($this->returnValue(NULL)); + + $this->policy->addPolicy($rule); + + $result = $this->policy->check($this->response, $this->request); + $this->assertSame(NULL, $result); + } + + /** + * Asserts that check() throws an exception if a rule returns an invalid value. + * + * @expectedException \UnexpectedValueException + * @dataProvider providerChainExceptionOnInvalidReturnValue + * @covers ::check + */ + public function testChainExceptionOnInvalidReturnValue($return_value) { + $rule = $this->getMock('Drupal\Core\PageCache\ResponsePolicyInterface'); + $rule->expects($this->once()) + ->method('check') + ->with($this->response, $this->request) + ->will($this->returnValue($return_value)); + + $this->policy->addPolicy($rule); + + $actual_result = $this->policy->check($this->response, $this->request); + $this->assertSame(NULL, $actual_result); + } + + /** + * Provides test data for testChainExceptionOnInvalidReturnValue. + * + * @return array + * Test input and expected result. + */ + public function providerChainExceptionOnInvalidReturnValue() { + return [ + [FALSE], + [0], + [1], + [TRUE], + [[1, 2, 3]], + [new \stdClass()], + ]; + } + + /** + * Asserts that check() returns immediately when a rule returned DENY. + */ + public function testStopChainOnFirstDeny() { + $rule1 = $this->getMock('Drupal\Core\PageCache\ResponsePolicyInterface'); + $rule1->expects($this->once()) + ->method('check') + ->with($this->response, $this->request); + $this->policy->addPolicy($rule1); + + $deny_rule = $this->getMock('Drupal\Core\PageCache\ResponsePolicyInterface'); + $deny_rule->expects($this->once()) + ->method('check') + ->with($this->response, $this->request) + ->will($this->returnValue(ResponsePolicyInterface::DENY)); + $this->policy->addPolicy($deny_rule); + + $ignored_rule = $this->getMock('Drupal\Core\PageCache\ResponsePolicyInterface'); + $ignored_rule->expects($this->never()) + ->method('check'); + $this->policy->addPolicy($ignored_rule); + + $actual_result = $this->policy->check($this->response, $this->request); + $this->assertsame(ResponsePolicyInterface::DENY, $actual_result); + } + +} diff --git a/core/tests/Drupal/Tests/Core/PageCache/CommandLineOrUnsafeMethodTest.php b/core/tests/Drupal/Tests/Core/PageCache/CommandLineOrUnsafeMethodTest.php new file mode 100644 index 0000000..bb54fbc --- /dev/null +++ b/core/tests/Drupal/Tests/Core/PageCache/CommandLineOrUnsafeMethodTest.php @@ -0,0 +1,83 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\PageCache\CommandLineOrUnsafeMethodTest. + */ + +namespace Drupal\Tests\Core\PageCache; + +use Drupal\Core\PageCache\RequestPolicyInterface; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\HttpFoundation\Request; + +/** + * @coversDefaultClass \Drupal\Core\PageCache\RequestPolicy\CommandLineOrUnsafeMethod + * @group PageCache + */ +class CommandLineOrUnsafeMethodTest extends UnitTestCase { + + /** + * The request policy under test. + * + * @var \Drupal\Core\PageCache\RequestPolicy\CommandLineOrUnsafeMethod|\PHPUnit_Framework_MockObject_MockObject + */ + protected $policy; + + public function setUp() { + // Note that it is necessary to partially mock the class under test in + // order to disable the isCli-check. + $this->policy = $this->getMock('Drupal\Core\PageCache\RequestPolicy\CommandLineOrUnsafeMethod', array('isCli')); + } + + /** + * Asserts that check() returns DENY for unsafe HTTP methods. + * + * @dataProvider providerTestHttpMethod + * @covers ::check + */ + public function testHttpMethod($expected_result, $method) { + $this->policy->expects($this->once()) + ->method('isCli') + ->will($this->returnValue(FALSE)); + + $request = Request::create('/', $method); + $actual_result = $this->policy->check($request); + $this->assertSame($expected_result, $actual_result); + } + + /** + * Provides test data and expected results for the HTTP method test. + * + * @return array + * Test data and expected results. + */ + public function providerTestHttpMethod() { + return [ + [NULL, 'GET'], + [NULL, 'HEAD'], + [RequestPolicyInterface::DENY, 'POST'], + [RequestPolicyInterface::DENY, 'PUT'], + [RequestPolicyInterface::DENY, 'DELETE'], + [RequestPolicyInterface::DENY, 'OPTIONS'], + [RequestPolicyInterface::DENY, 'TRACE'], + [RequestPolicyInterface::DENY, 'CONNECT'], + ]; + } + + /** + * Asserts that check() returns DENY if running from the command line. + * + * @covers ::check + */ + public function testIsCli() { + $this->policy->expects($this->once()) + ->method('isCli') + ->will($this->returnValue(TRUE)); + + $request = Request::create('/', 'GET'); + $actual_result = $this->policy->check($request); + $this->assertSame(RequestPolicyInterface::DENY, $actual_result); + } + +} diff --git a/core/tests/Drupal/Tests/Core/PageCache/NoSessionOpenTest.php b/core/tests/Drupal/Tests/Core/PageCache/NoSessionOpenTest.php new file mode 100644 index 0000000..f0fe4cf --- /dev/null +++ b/core/tests/Drupal/Tests/Core/PageCache/NoSessionOpenTest.php @@ -0,0 +1,54 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\PageCache\NoSessionOpenTest. + */ + +namespace Drupal\Tests\Core\PageCache; + +use Drupal\Core\PageCache\RequestPolicyInterface; +use Drupal\Core\PageCache\RequestPolicy; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\HttpFoundation\Request; + +/** + * @coversDefaultClass \Drupal\Core\PageCache\RequestPolicy\NoSessionOpen + * @group PageCache + */ +class NoSessionOpenTest extends UnitTestCase { + + /** + * The session cookie name. + * + * @var string + */ + protected $sessionCookieName; + + /** + * The request policy under test. + * + * @var \Drupal\Core\PageCache\RequestPolicy\NoSessionOpen + */ + protected $policy; + + public function setUp() { + $this->sessionCookieName = 'B1ESkdf3V4F8u27myaSAShuuHc'; + $this->policy = new RequestPolicy\NoSessionOpen($this->sessionCookieName); + } + + /** + * Asserts that caching is allowed unless there is a session cookie present. + * + * @covers ::check + */ + public function testNoAllowUnlessSessionCookiePresent() { + $request = new Request(); + $result = $this->policy->check($request); + $this->assertSame(RequestPolicyInterface::ALLOW, $result); + + $request = Request::create('/', 'GET', [], [$this->sessionCookieName => 'some-session-id']); + $result = $this->policy->check($request); + $this->assertSame(NULL, $result); + } +} |