View source
<?php
namespace Drupal\Tests\page_cache\Functional;
use Drupal\Component\Datetime\DateTimePlus;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\user\RoleInterface;
class PageCacheTest extends BrowserTestBase {
use AssertPageCacheContextsAndTagsTrait;
protected $dumpHeaders = TRUE;
protected static $modules = [
'test_page_test',
'system_test',
'entity_test',
];
protected $defaultTheme = 'stark';
protected function setUp() : void {
parent::setUp();
$this
->config('system.site')
->set('name', 'Drupal')
->set('page.front', '/test-page')
->save();
}
public function testPageCacheTags() {
$config = $this
->config('system.performance');
$config
->set('cache.page.max_age', 300);
$config
->save();
$path = 'system-test/cache_tags_page';
$tags = [
'system_test_cache_tags_page',
];
$this
->drupalGet($path);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->drupalGet($path);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$cid_parts = [
Url::fromRoute('system_test.cache_tags_page', [], [
'absolute' => TRUE,
])
->toString(),
'',
];
$cid = implode(':', $cid_parts);
$cache_entry = \Drupal::cache('page')
->get($cid);
sort($cache_entry->tags);
$expected_tags = [
'config:user.role.anonymous',
'http_response',
'pre_render',
'rendered',
'system_test_cache_tags_page',
];
$this
->assertSame($expected_tags, $cache_entry->tags);
Cache::invalidateTags($tags);
$this
->drupalGet($path);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
}
public function testPageCacheTagsIndependentFromCacheabilityHeaders() {
$this
->setContainerParameter('http.response.debug_cacheability_headers', FALSE);
$this
->rebuildContainer();
$this
->resetAll();
$path = 'system-test/cache_tags_page';
$tags = [
'system_test_cache_tags_page',
];
$this
->drupalGet($path);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->drupalGet($path);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$cid_parts = [
Url::fromRoute('system_test.cache_tags_page', [], [
'absolute' => TRUE,
])
->toString(),
'',
];
$cid = implode(':', $cid_parts);
$cache_entry = \Drupal::cache('page')
->get($cid);
sort($cache_entry->tags);
$expected_tags = [
'config:user.role.anonymous',
'http_response',
'pre_render',
'rendered',
'system_test_cache_tags_page',
];
$this
->assertSame($expected_tags, $cache_entry->tags);
Cache::invalidateTags($tags);
$this
->drupalGet($path);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
}
public function testQueryParameterFormatRequests() {
$config = $this
->config('system.performance');
$config
->set('cache.page.max_age', 300);
$config
->save();
$accept_header_cache_url = Url::fromRoute('system_test.page_cache_accept_header');
$accept_header_cache_url_with_json = Url::fromRoute('system_test.page_cache_accept_header', [
'_format' => 'json',
]);
$this
->drupalGet($accept_header_cache_url);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->drupalGet($accept_header_cache_url);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$this
->assertSession()
->responseContains('<p>oh hai this is html.</p>');
$this
->drupalGet($accept_header_cache_url_with_json);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->drupalGet($accept_header_cache_url_with_json);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$this
->assertSession()
->responseContains('{"content":"oh hai this is json"}');
\Drupal::service('module_installer')
->install([
'node',
'rest',
'hal',
'basic_auth',
]);
$this
->drupalCreateContentType([
'type' => 'article',
]);
$node = $this
->drupalCreateNode([
'type' => 'article',
]);
$node_uri = $node
->toUrl();
$node_url_with_hal_json_format = $node
->toUrl('canonical')
->setRouteParameter('_format', 'hal_json');
$this
->drupalGet($node_uri);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->assertSession()
->responseHeaderEquals('Content-Type', 'text/html; charset=UTF-8');
$this
->drupalGet($node_uri);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$this
->assertSession()
->responseHeaderEquals('Content-Type', 'text/html; charset=UTF-8');
$this
->drupalGet($node_url_with_hal_json_format);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->assertSession()
->responseHeaderEquals('Content-Type', 'application/hal+json');
$this
->drupalGet($node_url_with_hal_json_format);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$this
->assertSession()
->responseHeaderEquals('Content-Type', 'application/hal+json');
\Drupal::cache('page')
->deleteAll();
$this
->drupalGet($node_url_with_hal_json_format);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->assertSession()
->responseHeaderEquals('Content-Type', 'application/hal+json');
$this
->drupalGet($node_url_with_hal_json_format);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$this
->assertSession()
->responseHeaderEquals('Content-Type', 'application/hal+json');
$this
->drupalGet($node_uri);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->assertSession()
->responseHeaderEquals('Content-Type', 'text/html; charset=UTF-8');
$this
->drupalGet($node_uri);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$this
->assertSession()
->responseHeaderEquals('Content-Type', 'text/html; charset=UTF-8');
}
public function testConditionalRequests() {
$config = $this
->config('system.performance');
$config
->set('cache.page.max_age', 300);
$config
->save();
$this
->drupalGet('');
$this
->assertSession()
->responseNotMatches('#<html.*<html#');
$this
->drupalGet('');
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$etag = $this
->getSession()
->getResponseHeader('ETag');
$last_modified = $this
->getSession()
->getResponseHeader('Last-Modified');
$this
->drupalGet('', [], [
'If-Modified-Since' => $last_modified,
'If-None-Match' => $etag,
]);
$this
->assertSession()
->statusCodeEquals(304);
$this
->drupalGet('', [], [
'If-Modified-Since' => gmdate(DATE_RFC822, strtotime($last_modified)),
'If-None-Match' => $etag,
]);
$this
->assertSession()
->statusCodeEquals(304);
$this
->drupalGet('', [], [
'If-Modified-Since' => gmdate(DATE_RFC850, strtotime($last_modified)),
'If-None-Match' => $etag,
]);
$this
->assertSession()
->statusCodeEquals(304);
$this
->drupalGet('', [], [
'If-Modified-Since' => $last_modified,
'If-None-Match' => NULL,
]);
$this
->assertSession()
->responseNotMatches('#<html.*<html#');
$this
->assertSession()
->statusCodeEquals(200);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$this
->drupalGet('', [], [
'If-Modified-Since' => gmdate(DateTimePlus::RFC7231, strtotime($last_modified) + 1),
'If-None-Match' => $etag,
]);
$this
->assertSession()
->statusCodeEquals(200);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$user = $this
->drupalCreateUser();
$this
->drupalLogin($user);
$this
->drupalGet('', [], [
'If-Modified-Since' => $last_modified,
'If-None-Match' => $etag,
]);
$this
->assertSession()
->statusCodeEquals(200);
$this
->assertSession()
->responseHeaderDoesNotExist('X-Drupal-Cache');
}
public function testPageCache() {
$config = $this
->config('system.performance');
$config
->set('cache.page.max_age', 300);
$config
->save();
$this
->drupalGet('system-test/set-header', [
'query' => [
'name' => 'Foo',
'value' => 'bar',
],
]);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->assertSession()
->responseHeaderContains('Vary', 'cookie');
$this
->assertSession()
->responseHeaderEquals('Cache-Control', 'max-age=300, public');
$this
->assertSession()
->responseHeaderEquals('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT');
$this
->assertSession()
->responseHeaderEquals('Foo', 'bar');
$this
->drupalGet('system-test/set-header', [
'query' => [
'name' => 'Foo',
'value' => 'bar',
],
]);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$this
->assertSession()
->responseHeaderContains('Vary', 'cookie');
$this
->assertSession()
->responseHeaderEquals('Cache-Control', 'max-age=300, public');
$this
->assertSession()
->responseHeaderEquals('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT');
$this
->assertSession()
->responseHeaderEquals('Foo', 'bar');
$this
->drupalGet('system-test/set-header', [
'query' => [
'name' => 'Expires',
'value' => 'Fri, 19 Nov 2008 05:00:00 GMT',
],
]);
$this
->assertSession()
->responseHeaderEquals('Expires', 'Fri, 19 Nov 2008 05:00:00 GMT');
$this
->drupalGet('system-test/set-header', [
'query' => [
'name' => 'Vary',
'value' => 'User-Agent',
],
]);
$this
->assertSession()
->responseHeaderContains('Vary', 'user-agent');
$user = $this
->drupalCreateUser();
$this
->drupalLogin($user);
$this
->drupalGet('system-test/set-header', [
'query' => [
'name' => 'Foo',
'value' => 'bar',
],
]);
$this
->assertSession()
->responseHeaderDoesNotExist('X-Drupal-Cache');
$this
->assertSession()
->responseHeaderNotContains('Vary', 'cookie');
$this
->assertSession()
->responseHeaderEquals('Cache-Control', 'must-revalidate, no-cache, private');
$this
->assertSession()
->responseHeaderEquals('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT');
$this
->assertSession()
->responseHeaderEquals('Foo', 'bar');
$this
->drupalLogout();
$this
->drupalGet('system-test/cache_maxage_page');
$this
->assertSession()
->responseHeaderEquals('Cache-Control', 'max-age=300, public');
}
public function testPageCacheAnonymousRolePermissions() {
$config = $this
->config('system.performance');
$config
->set('cache.page.max_age', 300);
$config
->save();
$content_url = Url::fromRoute('system_test.permission_dependent_content');
$route_access_url = Url::fromRoute('system_test.permission_dependent_route_access');
$this
->drupalGet($content_url);
$this
->assertSession()
->pageTextContains('Permission to pet llamas: no!');
$this
->assertCacheContext('user.permissions');
$this
->assertSession()
->responseHeaderContains('X-Drupal-Cache-Tags', 'config:user.role.anonymous');
$this
->drupalGet($route_access_url);
$this
->assertCacheContext('user.permissions');
$this
->assertSession()
->responseHeaderContains('X-Drupal-Cache-Tags', 'config:user.role.anonymous');
user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, [
'pet llamas',
]);
$this
->drupalGet($content_url);
$this
->assertSession()
->pageTextContains('Permission to pet llamas: yes!');
$this
->assertCacheContext('user.permissions');
$this
->assertSession()
->responseHeaderContains('X-Drupal-Cache-Tags', 'config:user.role.anonymous');
$this
->drupalGet($route_access_url);
$this
->assertCacheContext('user.permissions');
$this
->assertSession()
->responseHeaderContains('X-Drupal-Cache-Tags', 'config:user.role.anonymous');
$auth_user = $this
->drupalCreateUser();
$this
->drupalLogin($auth_user);
$this
->drupalGet($content_url);
$this
->assertSession()
->pageTextContains('Permission to pet llamas: no!');
$this
->assertCacheContext('user.permissions');
$this
->assertSession()
->responseHeaderNotContains('X-Drupal-Cache-Tags', 'config:user.role.authenticated');
$this
->drupalGet($route_access_url);
$this
->assertCacheContext('user.permissions');
$this
->assertSession()
->responseHeaderNotContains('X-Drupal-Cache-Tags', 'config:user.role.authenticated');
user_role_grant_permissions(RoleInterface::AUTHENTICATED_ID, [
'pet llamas',
]);
$this
->drupalGet($content_url);
$this
->assertSession()
->pageTextContains('Permission to pet llamas: yes!');
$this
->assertCacheContext('user.permissions');
$this
->assertSession()
->responseHeaderNotContains('X-Drupal-Cache-Tags', 'config:user.role.authenticated');
$this
->drupalGet($route_access_url);
$this
->assertCacheContext('user.permissions');
$this
->assertSession()
->responseHeaderNotContains('X-Drupal-Cache-Tags', 'config:user.role.authenticated');
}
public function testPageCacheAnonymous403404() {
$admin_url = Url::fromRoute('system.admin');
$invalid_url = 'foo/does_not_exist';
$tests = [
403 => $admin_url,
404 => $invalid_url,
];
$cache_ttl_4xx = Settings::get('cache_ttl_4xx', 3600);
foreach ($tests as $code => $content_url) {
$this
->drupalGet($content_url);
$this
->assertSession()
->statusCodeEquals($code);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->assertSession()
->responseHeaderContains('X-Drupal-Cache-Tags', '4xx-response');
$this
->drupalGet($content_url);
$this
->assertSession()
->statusCodeEquals($code);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$entity_values = [
'name' => $this
->randomMachineName(),
'user_id' => 1,
'field_test_text' => [
0 => [
'value' => $this
->randomString(),
'format' => 'plain_text',
],
],
];
$entity = EntityTest::create($entity_values);
$entity
->save();
$this
->drupalGet($content_url);
$this
->assertSession()
->statusCodeEquals($code);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->drupalGet($content_url);
$this
->assertSession()
->statusCodeEquals($code);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$this->container
->get('router.builder')
->rebuild();
$this
->drupalGet($content_url);
$this
->assertSession()
->statusCodeEquals($code);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$cache_item = \Drupal::service('cache.page')
->get($this
->getUrl() . ':');
$difference = $cache_item->expire - (int) $cache_item->created;
$this
->assertTrue($difference > $cache_ttl_4xx - 10 && $difference < $cache_ttl_4xx + 10, "The cache entry expiry time uses the cache_ttl_4xx setting. Expire: {$cache_item->expire} Created: {$cache_item->created}");
}
$settings['settings']['cache_ttl_4xx'] = (object) [
'value' => 0,
'required' => TRUE,
];
$this
->writeSettings($settings);
\Drupal::service('cache.page')
->deleteAll();
foreach ($tests as $code => $content_url) {
$this
->drupalGet($content_url);
$this
->drupalGet($content_url);
$this
->assertSession()
->statusCodeEquals($code);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
}
}
public function testPageCacheWithoutVaryCookie() {
$config = $this
->config('system.performance');
$config
->set('cache.page.max_age', 300);
$config
->save();
$settings['settings']['omit_vary_cookie'] = (object) [
'value' => TRUE,
'required' => TRUE,
];
$this
->writeSettings($settings);
$this
->drupalGet('');
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->assertSession()
->responseHeaderNotContains('Vary', 'cookie');
$this
->assertSession()
->responseHeaderEquals('Cache-Control', 'max-age=300, public');
$this
->drupalGet('');
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$this
->assertSession()
->responseHeaderNotContains('Vary', 'cookie');
$this
->assertSession()
->responseHeaderEquals('Cache-Control', 'max-age=300, public');
}
public function testFormImmutability() {
$this->container
->get('module_installer')
->install([
'page_cache_form_test',
]);
$this->container
->get('module_installer')
->uninstall([
'page_cache',
]);
$this
->drupalGet('page_cache_form_test_immutability');
$this
->assertSession()
->pageTextContains("Immutable: TRUE");
\Drupal::state()
->set('page_cache_bypass_form_immutability', TRUE);
\Drupal::moduleHandler()
->resetImplementations();
Cache::invalidateTags([
'rendered',
]);
$this
->drupalGet('page_cache_form_test_immutability');
$this
->assertSession()
->pageTextContains("Immutable: FALSE");
}
public function testCacheableResponseResponses() {
$config = $this
->config('system.performance');
$config
->set('cache.page.max_age', 300);
$config
->save();
$this
->drupalGet('/system-test/respond-response');
$this
->assertSession()
->responseHeaderDoesNotExist('X-Drupal-Cache');
$this
->assertSession()
->responseHeaderEquals('Cache-Control', 'must-revalidate, no-cache, private');
$this
->drupalGet('/system-test/respond-response');
$this
->assertSession()
->responseHeaderDoesNotExist('X-Drupal-Cache');
$this
->assertSession()
->responseHeaderEquals('Cache-Control', 'must-revalidate, no-cache, private');
$this
->drupalGet('/system-test/respond-public-response');
$this
->assertSession()
->responseHeaderDoesNotExist('X-Drupal-Cache');
$this
->assertSession()
->responseHeaderEquals('Cache-Control', 'max-age=60, public');
$this
->drupalGet('/system-test/respond-public-response');
$this
->assertSession()
->responseHeaderDoesNotExist('X-Drupal-Cache');
$this
->assertSession()
->responseHeaderEquals('Cache-Control', 'max-age=60, public');
$this
->drupalGet('/system-test/respond-cacheable-response');
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->assertSession()
->responseHeaderEquals('Cache-Control', 'max-age=300, public');
$this
->drupalGet('/system-test/respond-cacheable-response');
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$this
->assertSession()
->responseHeaderEquals('Cache-Control', 'max-age=300, public');
$this->container
->get('module_installer')
->uninstall([
'page_cache',
]);
$this
->drupalGet('/respond-cacheable-response');
$this
->assertSession()
->responseHeaderDoesNotExist('X-Drupal-Cache');
}
public function testHead() {
$client = $this
->getSession()
->getDriver()
->getClient()
->getClient();
$url_a = $this
->buildUrl('system-test/set-header', [
'query' => [
'name' => 'Foo',
'value' => 'bar',
],
]);
$response_body = $this
->drupalGet($url_a);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'MISS');
$this
->assertSession()
->responseHeaderEquals('Foo', 'bar');
$this
->assertEquals('The following header was set: <em class="placeholder">Foo</em>: <em class="placeholder">bar</em>', $response_body);
$response = $client
->request('HEAD', $url_a);
$this
->assertEquals('HIT', $response
->getHeaderLine('X-Drupal-Cache'), 'Page was cached.');
$this
->assertEquals('bar', $response
->getHeaderLine('Foo'), 'Custom header was sent.');
$this
->assertEquals('', $response
->getBody()
->getContents());
$url_b = $this
->buildUrl('system-test/set-header', [
'query' => [
'name' => 'Foo',
'value' => 'baz',
],
]);
$response = $client
->request('HEAD', $url_b);
$this
->assertEquals('MISS', $response
->getHeaderLine('X-Drupal-Cache'), 'Page was not cached.');
$this
->assertEquals('baz', $response
->getHeaderLine('Foo'), 'Custom header was sent.');
$this
->assertEquals('', $response
->getBody()
->getContents());
$response_body = $this
->drupalGet($url_b);
$this
->assertSession()
->responseHeaderEquals('X-Drupal-Cache', 'HIT');
$this
->assertSession()
->responseHeaderEquals('Foo', 'baz');
$this
->assertEquals('The following header was set: <em class="placeholder">Foo</em>: <em class="placeholder">baz</em>', $response_body);
}
public function testCacheableWithCustomCacheControl() {
$config = $this
->config('system.performance');
$config
->set('cache.page.max_age', 300);
$config
->save();
$this
->drupalGet('/system-test/custom-cache-control');
$this
->assertSession()
->statusCodeEquals(200);
$this
->assertSession()
->responseHeaderEquals('Cache-Control', 'bar, private');
}
public function testNoUrlNormalization() {
$url = Url::fromRoute('<front>')
->setAbsolute()
->toString();
$tests = [
[
$url . '?z=z&a=a',
$url . '?a=a&z=z',
],
[
$url . '?a=b+c',
$url . '?a=b%20c',
],
];
foreach ($tests as list($url_raw, $url_normalized)) {
$headers = $this
->getHeaders($url_raw);
$this
->assertEquals('MISS', $headers['X-Drupal-Cache']);
$headers = $this
->getHeaders($url_raw);
$this
->assertEquals('HIT', $headers['X-Drupal-Cache'], "Cache was set for {$url_raw} URL.");
$headers = $this
->getHeaders($url_normalized);
$this
->assertEquals('MISS', $headers['X-Drupal-Cache'], "Cache is missing for {$url_normalized} URL.");
}
}
protected function getHeaders($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, TRUE);
curl_setopt($ch, CURLOPT_NOBODY, TRUE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_USERAGENT, drupal_generate_test_ua($this->databasePrefix));
$output = curl_exec($ch);
curl_close($ch);
$headers = [];
foreach (explode("\n", $output) as $header) {
if (strpos($header, ':')) {
list($key, $value) = explode(':', $header, 2);
$headers[trim($key)] = trim($value);
}
}
return $headers;
}
}