mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
NEW Add better HTTP cache-control manipulation (#8086)
This commit is contained in:
parent
1d0cffd0e4
commit
2b4954035f
@ -13,13 +13,19 @@ MySQLDatabase:
|
|||||||
collation: utf8_general_ci
|
collation: utf8_general_ci
|
||||||
HTTP:
|
HTTP:
|
||||||
cache_control:
|
cache_control:
|
||||||
max-age: 0
|
no-cache: "true"
|
||||||
|
no-store: "true"
|
||||||
must-revalidate: "true"
|
must-revalidate: "true"
|
||||||
no-transform: "true"
|
vary: "X-Requested-With, X-Forwarded-Protocol"
|
||||||
vary: "Cookie, X-Forwarded-Protocol, User-Agent, Accept"
|
|
||||||
LeftAndMain:
|
LeftAndMain:
|
||||||
dependencies:
|
dependencies:
|
||||||
versionProvider: %$SilverStripeVersionProvider
|
versionProvider: %$SilverStripeVersionProvider
|
||||||
SilverStripeVersionProvider:
|
SilverStripeVersionProvider:
|
||||||
modules:
|
modules:
|
||||||
silverstripe/framework: Framework
|
silverstripe/framework: Framework
|
||||||
|
---
|
||||||
|
Only:
|
||||||
|
environment: dev
|
||||||
|
---
|
||||||
|
HTTP:
|
||||||
|
disable_http_cache: true
|
||||||
|
@ -204,10 +204,7 @@ class RSSFeed extends ViewableData {
|
|||||||
HTTP::register_etag($this->etag);
|
HTTP::register_etag($this->etag);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!headers_sent()) {
|
$response->addHeader("Content-Type", "application/rss+xml; charset=utf-8");
|
||||||
HTTP::add_cache_headers();
|
|
||||||
$response->addHeader("Content-Type", "application/rss+xml; charset=utf-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
Config::inst()->update('SSViewer', 'source_file_comments', $prevState);
|
Config::inst()->update('SSViewer', 'source_file_comments', $prevState);
|
||||||
|
|
||||||
|
@ -169,10 +169,7 @@ class Controller extends RequestHandler implements TemplateGlobalProvider {
|
|||||||
$response->setBody($body);
|
$response->setBody($body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ContentNegotiator::process($response);
|
ContentNegotiator::process($response);
|
||||||
HTTP::add_cache_headers($response);
|
|
||||||
|
|
||||||
$this->popCurrent();
|
$this->popCurrent();
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
@ -502,7 +499,7 @@ class Controller extends RequestHandler implements TemplateGlobalProvider {
|
|||||||
*/
|
*/
|
||||||
public function redirectBack() {
|
public function redirectBack() {
|
||||||
// Don't cache the redirect back ever
|
// Don't cache the redirect back ever
|
||||||
HTTP::set_cache_age(0);
|
HTTPCacheControl::singleton()->disableCache(true);
|
||||||
|
|
||||||
$url = null;
|
$url = null;
|
||||||
|
|
||||||
|
@ -386,7 +386,14 @@ class Director implements TemplateGlobalProvider {
|
|||||||
} catch(SS_HTTPResponse_Exception $responseException) {
|
} catch(SS_HTTPResponse_Exception $responseException) {
|
||||||
$result = $responseException->getResponse();
|
$result = $responseException->getResponse();
|
||||||
}
|
}
|
||||||
if(!is_object($result) || $result instanceof SS_HTTPResponse) return $result;
|
// Ensure cache headers are added
|
||||||
|
if ($result instanceof SS_HTTPResponse) {
|
||||||
|
HTTP::add_cache_headers($result);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
if(!is_object($result)) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
user_error("Bad result from url " . $request->getURL() . " handled by " .
|
user_error("Bad result from url " . $request->getURL() . " handled by " .
|
||||||
get_class($controllerObj)." controller: ".get_class($result), E_USER_WARNING);
|
get_class($controllerObj)." controller: ".get_class($result), E_USER_WARNING);
|
||||||
|
@ -18,6 +18,9 @@ class FlushRequestFilter implements RequestFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function postRequest(SS_HTTPRequest $request, SS_HTTPResponse $response, DataModel $model) {
|
public function postRequest(SS_HTTPRequest $request, SS_HTTPResponse $response, DataModel $model) {
|
||||||
|
if(array_key_exists('flush', $request->getVars())) {
|
||||||
|
HTTPCacheControl::singleton()->disableCache(true);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
281
control/HTTP.php
281
control/HTTP.php
@ -6,6 +6,7 @@
|
|||||||
*
|
*
|
||||||
* @package framework
|
* @package framework
|
||||||
* @subpackage misc
|
* @subpackage misc
|
||||||
|
* @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
|
||||||
*/
|
*/
|
||||||
class HTTP {
|
class HTTP {
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ class HTTP {
|
|||||||
protected static $cache_age = 0;
|
protected static $cache_age = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var timestamp $modification_date
|
* @var int $modification_date
|
||||||
*/
|
*/
|
||||||
protected static $modification_date = null;
|
protected static $modification_date = null;
|
||||||
|
|
||||||
@ -29,15 +30,48 @@ class HTTP {
|
|||||||
*/
|
*/
|
||||||
private static $cache_ajax_requests = true;
|
private static $cache_ajax_requests = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @config
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private static $disable_http_cache = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of extension to mime types
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
* @config
|
||||||
|
*/
|
||||||
|
private static $MimeTypes = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of names to add to the Cache-Control header.
|
||||||
|
*
|
||||||
|
* @see HTTPCacheControl::__construct()
|
||||||
|
* @config
|
||||||
|
* @var array Keys are cache control names, values are boolean flags
|
||||||
|
*/
|
||||||
|
private static $cache_control = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vary string; A comma separated list of var header names
|
||||||
|
*
|
||||||
|
* @config
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
private static $vary = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns a local system filename into a URL by comparing it to the script
|
* Turns a local system filename into a URL by comparing it to the script
|
||||||
* filename.
|
* filename.
|
||||||
*
|
*
|
||||||
* @param string
|
* @param string
|
||||||
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function filename2url($filename) {
|
public static function filename2url($filename) {
|
||||||
$slashPos = -1;
|
$slashPos = -1;
|
||||||
|
|
||||||
|
$commonLength = null;
|
||||||
while(($slashPos = strpos($filename, "/", $slashPos+1)) !== false) {
|
while(($slashPos = strpos($filename, "/", $slashPos+1)) !== false) {
|
||||||
if(substr($filename, 0, $slashPos) == substr($_SERVER['SCRIPT_FILENAME'],0,$slashPos)) {
|
if(substr($filename, 0, $slashPos) == substr($_SERVER['SCRIPT_FILENAME'],0,$slashPos)) {
|
||||||
$commonLength = $slashPos;
|
$commonLength = $slashPos;
|
||||||
@ -63,6 +97,9 @@ class HTTP {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Turn all relative URLs in the content to absolute URLs
|
* Turn all relative URLs in the content to absolute URLs
|
||||||
|
*
|
||||||
|
* @param string $html
|
||||||
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function absoluteURLs($html) {
|
public static function absoluteURLs($html) {
|
||||||
$html = str_replace('$CurrentPageURL', $_SERVER['REQUEST_URI'], $html);
|
$html = str_replace('$CurrentPageURL', $_SERVER['REQUEST_URI'], $html);
|
||||||
@ -99,7 +136,7 @@ class HTTP {
|
|||||||
* @param string|callable $code Either a string that can evaluate to an expression
|
* @param string|callable $code Either a string that can evaluate to an expression
|
||||||
* to rewrite links (depreciated), or a callable that takes a single
|
* to rewrite links (depreciated), or a callable that takes a single
|
||||||
* parameter and returns the rewritten URL
|
* parameter and returns the rewritten URL
|
||||||
* @return The content with all links rewritten as per the logic specified in $code
|
* @return string The content with all links rewritten as per the logic specified in $code
|
||||||
*/
|
*/
|
||||||
public static function urlRewriter($content, $code) {
|
public static function urlRewriter($content, $code) {
|
||||||
if(!is_callable($code)) {
|
if(!is_callable($code)) {
|
||||||
@ -107,6 +144,7 @@ class HTTP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Replace attributes
|
// Replace attributes
|
||||||
|
$regExps = array();
|
||||||
$attribs = array("src","background","a" => "href","link" => "href", "base" => "href");
|
$attribs = array("src","background","a" => "href","link" => "href", "base" => "href");
|
||||||
foreach($attribs as $tag => $attrib) {
|
foreach($attribs as $tag => $attrib) {
|
||||||
if(!is_numeric($tag)) $tagPrefix = "$tag ";
|
if(!is_numeric($tag)) $tagPrefix = "$tag ";
|
||||||
@ -132,6 +170,7 @@ class HTTP {
|
|||||||
} else {
|
} else {
|
||||||
// Expose the $URL variable to be used by the $code expression
|
// Expose the $URL variable to be used by the $code expression
|
||||||
$URL = $matches[2];
|
$URL = $matches[2];
|
||||||
|
array($URL); // Ensure $URL is available to scope of below code
|
||||||
$rewritten = eval("return ($code);");
|
$rewritten = eval("return ($code);");
|
||||||
}
|
}
|
||||||
return $matches[1] . $rewritten . $matches[3];
|
return $matches[1] . $rewritten . $matches[3];
|
||||||
@ -285,9 +324,12 @@ class HTTP {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the maximum age of this page in web caches, in seconds
|
* Set the maximum age of this page in web caches, in seconds
|
||||||
|
*
|
||||||
|
* @param int $age
|
||||||
*/
|
*/
|
||||||
public static function set_cache_age($age) {
|
public static function set_cache_age($age) {
|
||||||
self::$cache_age = $age;
|
self::$cache_age = $age;
|
||||||
|
HTTPCacheControl::singleton()->setMaxAge($age);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function register_modification_date($dateString) {
|
public static function register_modification_date($dateString) {
|
||||||
@ -318,139 +360,129 @@ class HTTP {
|
|||||||
* deprecated; in these cases, the headers are output directly.
|
* deprecated; in these cases, the headers are output directly.
|
||||||
*/
|
*/
|
||||||
public static function add_cache_headers($body = null) {
|
public static function add_cache_headers($body = null) {
|
||||||
$cacheAge = self::$cache_age;
|
|
||||||
|
|
||||||
// Validate argument
|
// Validate argument
|
||||||
if($body && !($body instanceof SS_HTTPResponse)) {
|
if($body && !($body instanceof SS_HTTPResponse)) {
|
||||||
user_error("HTTP::add_cache_headers() must be passed an SS_HTTPResponse object", E_USER_WARNING);
|
user_error("HTTP::add_cache_headers() must be passed an SS_HTTPResponse object", E_USER_WARNING);
|
||||||
$body = null;
|
$body = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Development sites have frequently changing templates; this can get stuffed up by the code
|
|
||||||
// below.
|
|
||||||
if(Director::isDev()) $cacheAge = 0;
|
|
||||||
|
|
||||||
// The headers have been sent and we don't have an SS_HTTPResponse object to attach things to; no point in
|
// The headers have been sent and we don't have an SS_HTTPResponse object to attach things to; no point in
|
||||||
// us trying.
|
// us trying.
|
||||||
if(headers_sent() && !$body) return;
|
if(headers_sent() && !$body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn if already assigned cache-control headers
|
||||||
|
if ($body && $body->getHeader('Cache-Control')) {
|
||||||
|
trigger_error(
|
||||||
|
'Cache-Control header has already been set. '
|
||||||
|
. 'Please use HTTPCacheControl API to set caching options instead.',
|
||||||
|
E_USER_WARNING
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = Config::inst()->forClass(__CLASS__);
|
||||||
|
|
||||||
|
// Get current cache control state
|
||||||
|
$cacheControl = HTTPCacheControl::singleton();
|
||||||
|
|
||||||
|
// if http caching is disabled by config, disable it - used on dev environments due to frequently changing
|
||||||
|
// templates and other data. will be overridden by forced publicCache() or privateCache() calls
|
||||||
|
if ($config->get('disable_http_cache')) {
|
||||||
|
$cacheControl->disableCache();
|
||||||
|
}
|
||||||
|
|
||||||
// Populate $responseHeaders with all the headers that we want to build
|
// Populate $responseHeaders with all the headers that we want to build
|
||||||
$responseHeaders = array();
|
$responseHeaders = array();
|
||||||
|
|
||||||
$config = Config::inst();
|
// if no caching ajax requests, disable ajax if is ajax request
|
||||||
$cacheControlHeaders = Config::inst()->get('HTTP', 'cache_control');
|
if (!$config->get('cache_ajax_requests') && Director::is_ajax()) {
|
||||||
|
$cacheControl->disableCache();
|
||||||
|
|
||||||
// currently using a config setting to cancel this, seems to be so that the CMS caches ajax requests
|
|
||||||
if(function_exists('apache_request_headers') && $config->get(get_called_class(), 'cache_ajax_requests')) {
|
|
||||||
$requestHeaders = array_change_key_case(apache_request_headers(), CASE_LOWER);
|
|
||||||
|
|
||||||
if(isset($requestHeaders['x-requested-with']) && $requestHeaders['x-requested-with']=='XMLHttpRequest') {
|
|
||||||
$cacheAge = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if($cacheAge > 0) {
|
// Errors disable cache (unless some errors are cached intentionally by usercode)
|
||||||
$cacheControlHeaders['max-age'] = self::$cache_age;
|
if ($body && $body->isError()) {
|
||||||
|
// Even if publicCache(true) is specfied, errors will be uncachable
|
||||||
// Set empty pragma to avoid PHP's session_cache_limiter adding conflicting caching information,
|
$cacheControl->disableCache(true);
|
||||||
// defaulting to "nocache" on most PHP configurations (see http://php.net/session_cache_limiter).
|
|
||||||
// Since it's a deprecated HTTP 1.0 option, all modern HTTP clients and proxies should
|
|
||||||
// prefer the caching information indicated through the "Cache-Control" header.
|
|
||||||
$responseHeaders["Pragma"] = "";
|
|
||||||
|
|
||||||
// To do: User-Agent should only be added in situations where you *are* actually
|
|
||||||
// varying according to user-agent.
|
|
||||||
$vary = $config->get('HTTP', 'vary');
|
|
||||||
if ($vary && strlen($vary)) {
|
|
||||||
$responseHeaders['Vary'] = $vary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if($body) {
|
|
||||||
// Grab header for checking. Unfortunately HTTPRequest uses a mistyped variant.
|
|
||||||
$contentDisposition = $body->getHeader('Content-disposition');
|
|
||||||
if (!$contentDisposition) $contentDisposition = $body->getHeader('Content-Disposition');
|
|
||||||
}
|
|
||||||
|
|
||||||
if(
|
|
||||||
$body &&
|
|
||||||
Director::is_https() &&
|
|
||||||
isset($_SERVER['HTTP_USER_AGENT']) &&
|
|
||||||
strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')==true &&
|
|
||||||
strstr($contentDisposition, 'attachment;')==true
|
|
||||||
) {
|
|
||||||
// IE6-IE8 have problems saving files when https and no-cache are used
|
|
||||||
// (http://support.microsoft.com/kb/323308)
|
|
||||||
// Note: this is also fixable by ticking "Do not save encrypted pages to disk" in advanced options.
|
|
||||||
$cacheControlHeaders['max-age'] = 3;
|
|
||||||
|
|
||||||
// Set empty pragma to avoid PHP's session_cache_limiter adding conflicting caching information,
|
|
||||||
// defaulting to "nocache" on most PHP configurations (see http://php.net/session_cache_limiter).
|
|
||||||
// Since it's a deprecated HTTP 1.0 option, all modern HTTP clients and proxies should
|
|
||||||
// prefer the caching information indicated through the "Cache-Control" header.
|
|
||||||
$responseHeaders["Pragma"] = "";
|
|
||||||
} else {
|
|
||||||
$cacheControlHeaders['no-cache'] = "true";
|
|
||||||
$cacheControlHeaders['no-store'] = "true";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach($cacheControlHeaders as $header => $value) {
|
// If sessions exist we assume that the responses should not be cached by CDNs / proxies as we are
|
||||||
if(is_null($value)) {
|
// likely to be supplying information relevant to the current user only
|
||||||
unset($cacheControlHeaders[$header]);
|
if (Session::get_all()) {
|
||||||
} elseif((is_bool($value) && $value) || $value === "true") {
|
// Don't force in case user code chooses to opt in to public caching
|
||||||
$cacheControlHeaders[$header] = $header;
|
$cacheControl->privateCache();
|
||||||
} else {
|
|
||||||
$cacheControlHeaders[$header] = $header."=".$value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$responseHeaders['Cache-Control'] = implode(', ', $cacheControlHeaders);
|
// split the current vary header into it's parts and merge it with the config settings
|
||||||
unset($cacheControlHeaders, $header, $value);
|
// to create a list of unique vary values
|
||||||
|
$configVary = $config->get('vary');
|
||||||
|
$bodyVary = $body ? $body->getHeader('Vary') : '';
|
||||||
|
$vary = self::combineVary($configVary, $bodyVary);
|
||||||
|
if ($vary) {
|
||||||
|
$responseHeaders['Vary'] = $vary;
|
||||||
|
}
|
||||||
|
|
||||||
if(self::$modification_date && $cacheAge > 0) {
|
// deal with IE6-IE8 problems with https and no-cache
|
||||||
|
$contentDisposition = null;
|
||||||
|
if($body) {
|
||||||
|
// Grab header for checking. Unfortunately HTTPRequest uses a mistyped variant.
|
||||||
|
$contentDisposition = $body->getHeader('Content-Disposition', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(
|
||||||
|
$body &&
|
||||||
|
Director::is_https() &&
|
||||||
|
isset($_SERVER['HTTP_USER_AGENT']) &&
|
||||||
|
strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE') == true &&
|
||||||
|
strstr($contentDisposition, 'attachment;') == true &&
|
||||||
|
($cacheControl->hasDirective('no-cache') || $cacheControl->hasDirective('no-store'))
|
||||||
|
) {
|
||||||
|
// IE6-IE8 have problems saving files when https and no-cache/no-store are used
|
||||||
|
// (http://support.microsoft.com/kb/323308)
|
||||||
|
// Note: this is also fixable by ticking "Do not save encrypted pages to disk" in advanced options.
|
||||||
|
$cacheControl->privateCache(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$modification_date) {
|
||||||
$responseHeaders["Last-Modified"] = self::gmt_date(self::$modification_date);
|
$responseHeaders["Last-Modified"] = self::gmt_date(self::$modification_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we can store the cache responses we should generate and send etags
|
||||||
|
if (!$cacheControl->hasDirective('no-store')) {
|
||||||
// Chrome ignores Varies when redirecting back (http://code.google.com/p/chromium/issues/detail?id=79758)
|
// Chrome ignores Varies when redirecting back (http://code.google.com/p/chromium/issues/detail?id=79758)
|
||||||
// which means that if you log out, you get redirected back to a page which Chrome then checks against
|
// which means that if you log out, you get redirected back to a page which Chrome then checks against
|
||||||
// last-modified (which passes, getting a 304)
|
// last-modified (which passes, getting a 304)
|
||||||
// when it shouldn't be trying to use that page at all because it's the "logged in" version.
|
// when it shouldn't be trying to use that page at all because it's the "logged in" version.
|
||||||
// By also using and etag that includes both the modification date and all the varies
|
// By also using and etag that includes both the modification date and all the varies
|
||||||
// values which we also check against we can catch this and not return a 304
|
// values which we also check against we can catch this and not return a 304
|
||||||
$etagParts = array(self::$modification_date, serialize($_COOKIE));
|
$etag = self::generateETag($body);
|
||||||
$etagParts[] = Director::is_https() ? 'https' : 'http';
|
if ($etag) {
|
||||||
if (isset($_SERVER['HTTP_USER_AGENT'])) $etagParts[] = $_SERVER['HTTP_USER_AGENT'];
|
$responseHeaders['ETag'] = $etag;
|
||||||
if (isset($_SERVER['HTTP_ACCEPT'])) $etagParts[] = $_SERVER['HTTP_ACCEPT'];
|
|
||||||
|
|
||||||
$etag = sha1(implode(':', $etagParts));
|
// 304 response detection
|
||||||
$responseHeaders["ETag"] = $etag;
|
if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
|
||||||
|
// As above, only 304 if the last request had all the same varies values
|
||||||
|
// (or the etag isn't passed as part of the request - but with chrome it always is)
|
||||||
|
$matchesEtag = $_SERVER['HTTP_IF_NONE_MATCH'] == $etag;
|
||||||
|
|
||||||
// 304 response detection
|
if ($matchesEtag) {
|
||||||
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
|
if ($body) {
|
||||||
$ifModifiedSince = strtotime(stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']));
|
$body->setStatusCode(304);
|
||||||
|
$body->setBody('');
|
||||||
// As above, only 304 if the last request had all the same varies values
|
} else {
|
||||||
// (or the etag isn't passed as part of the request - but with chrome it always is)
|
// this is wrong, we need to send the same vary headers and so on
|
||||||
$matchesEtag = !isset($_SERVER['HTTP_IF_NONE_MATCH']) || $_SERVER['HTTP_IF_NONE_MATCH'] == $etag;
|
header('HTTP/1.0 304 Not Modified');
|
||||||
|
die();
|
||||||
if($ifModifiedSince >= self::$modification_date && $matchesEtag) {
|
}
|
||||||
if($body) {
|
|
||||||
$body->setStatusCode(304);
|
|
||||||
$body->setBody('');
|
|
||||||
} else {
|
|
||||||
header('HTTP/1.0 304 Not Modified');
|
|
||||||
die();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$expires = time() + $cacheAge;
|
|
||||||
$responseHeaders["Expires"] = self::gmt_date($expires);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(self::$etag) {
|
if ($cacheControl->hasDirective('max-age')) {
|
||||||
$responseHeaders['ETag'] = self::$etag;
|
$expires = time() + $cacheControl->getDirective('max-age');
|
||||||
|
$responseHeaders["Expires"] = self::gmt_date($expires);
|
||||||
}
|
}
|
||||||
|
|
||||||
// etag needs to be a quoted string according to HTTP spec
|
// etag needs to be a quoted string according to HTTP spec
|
||||||
@ -458,6 +490,9 @@ class HTTP {
|
|||||||
$responseHeaders['ETag'] = sprintf('"%s"', $responseHeaders['ETag']);
|
$responseHeaders['ETag'] = sprintf('"%s"', $responseHeaders['ETag']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge with cache control headers
|
||||||
|
$responseHeaders = array_merge($responseHeaders, $cacheControl->generateHeaders());
|
||||||
|
|
||||||
// Now that we've generated them, either output them or attach them to the SS_HTTPResponse as appropriate
|
// Now that we've generated them, either output them or attach them to the SS_HTTPResponse as appropriate
|
||||||
foreach($responseHeaders as $k => $v) {
|
foreach($responseHeaders as $k => $v) {
|
||||||
if($body) {
|
if($body) {
|
||||||
@ -471,23 +506,61 @@ class HTTP {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param SS_HTTPResponse|string $response
|
||||||
|
*
|
||||||
|
* @return string|false
|
||||||
|
*/
|
||||||
|
protected static function generateETag($response)
|
||||||
|
{
|
||||||
|
if (self::$etag) {
|
||||||
|
return self::$etag;
|
||||||
|
}
|
||||||
|
if ($response instanceof SS_HTTPResponse) {
|
||||||
|
return $response->getHeader('ETag') ?: sprintf('"%s"', md5($response->getBody()));
|
||||||
|
}
|
||||||
|
if ($response) {
|
||||||
|
return sprintf('"%s"', md5($response));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an {@link http://www.faqs.org/rfcs/rfc2822 RFC 2822} date in the
|
* Return an {@link http://www.faqs.org/rfcs/rfc2822 RFC 2822} date in the
|
||||||
* GMT timezone (a timestamp is always in GMT: the number of seconds
|
* GMT timezone (a timestamp is always in GMT: the number of seconds
|
||||||
* since January 1 1970 00:00:00 GMT)
|
* since January 1 1970 00:00:00 GMT)
|
||||||
|
*
|
||||||
|
* @param int $timestamp
|
||||||
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function gmt_date($timestamp) {
|
public static function gmt_date($timestamp) {
|
||||||
return gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';
|
return gmdate('D, d M Y H:i:s', $timestamp) . ' GMT';
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Return static variable cache_age in second
|
* Return static variable cache_age in second
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
*/
|
*/
|
||||||
public static function get_cache_age() {
|
public static function get_cache_age() {
|
||||||
return self::$cache_age;
|
return self::$cache_age;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine vary strings
|
||||||
|
*
|
||||||
|
* @param string $vary,... Each vary as a separate arg
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected static function combineVary($vary)
|
||||||
|
{
|
||||||
|
$varies = array();
|
||||||
|
foreach (func_get_args() as $arg) {
|
||||||
|
$argVaries = array_filter(preg_split("/\s*,\s*/", trim($arg)));
|
||||||
|
if ($argVaries) {
|
||||||
|
$varies = array_merge($varies, $argVaries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return implode(', ', array_unique($varies));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
442
control/HTTPCacheControl.php
Normal file
442
control/HTTPCacheControl.php
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class HTTPCacheControl
|
||||||
|
*
|
||||||
|
* @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
|
||||||
|
*/
|
||||||
|
class HTTPCacheControl extends SS_Object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var static
|
||||||
|
*/
|
||||||
|
private static $inst;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store for all the current directives and their values
|
||||||
|
* Starts with an implicit config for disabled caching
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $state = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcing level of previous setting; higher number wins
|
||||||
|
* Combination of consts belo
|
||||||
|
*w
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $forcingLevel = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcing level forced, optionally combined with one of the below.
|
||||||
|
*/
|
||||||
|
const LEVEL_FORCED = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcing level caching disabled. Overrides public/private.
|
||||||
|
*/
|
||||||
|
const LEVEL_DISABLED = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcing level private-cached. Overrides public.
|
||||||
|
*/
|
||||||
|
const LEVEL_PRIVATE = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcing level public cached. Lowest priority.
|
||||||
|
*/
|
||||||
|
const LEVEL_PUBLIC = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forcing level caching enabled.
|
||||||
|
*/
|
||||||
|
const LEVEL_ENABLED = 0;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of allowed cache directives for HTTPResponses
|
||||||
|
*
|
||||||
|
* This doesn't include any experimental directives,
|
||||||
|
* use the config system to add to these if you want to enable them
|
||||||
|
*
|
||||||
|
* @config
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private static $allowed_directives = array(
|
||||||
|
'public',
|
||||||
|
'private',
|
||||||
|
'no-cache',
|
||||||
|
'max-age',
|
||||||
|
's-maxage',
|
||||||
|
'must-revalidate',
|
||||||
|
'proxy-revalidate',
|
||||||
|
'no-store',
|
||||||
|
'no-transform',
|
||||||
|
);
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
// If we've not been provided an initial state, then grab HTTP.cache_contrpl from config
|
||||||
|
if (!$this->state) {
|
||||||
|
$this->setDirectivesFromArray(Config::inst()->get('HTTP', 'cache_control'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instruct the cache to apply a change with a given level, optionally
|
||||||
|
* modifying it with a force flag to increase priority of this action.
|
||||||
|
*
|
||||||
|
* If the apply level was successful, the change is made and the internal level
|
||||||
|
* threshold is incremented.
|
||||||
|
*
|
||||||
|
* @param int $level Priority of the given change
|
||||||
|
* @param bool $force If usercode has requested this action is forced to a higher priority.
|
||||||
|
* Note: Even if $force is set to true, other higher-priority forced changes can still
|
||||||
|
* cause a change to be rejected if it is below the required threshold.
|
||||||
|
* @return bool True if the given change is accepted, and that the internal
|
||||||
|
* level threshold is updated (if necessary) to the new minimum level.
|
||||||
|
*/
|
||||||
|
protected function applyChangeLevel($level, $force)
|
||||||
|
{
|
||||||
|
$forcingLevel = $level + ($force ? self::LEVEL_FORCED : 0);
|
||||||
|
if ($forcingLevel < $this->forcingLevel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$this->forcingLevel = $forcingLevel;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Low level method for setting directives include any experimental or custom ones added via config
|
||||||
|
*
|
||||||
|
* @param string $directive
|
||||||
|
* @param string|bool $value
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setDirective($directive, $value = null)
|
||||||
|
{
|
||||||
|
// make sure the directive is in the list of allowed directives
|
||||||
|
$allowedDirectives = $this->config()->get('allowed_directives');
|
||||||
|
$directive = strtolower($directive);
|
||||||
|
if (in_array($directive, $allowedDirectives)) {
|
||||||
|
$this->state[$directive] = $value;
|
||||||
|
} else {
|
||||||
|
throw new InvalidArgumentException('Directive ' . $directive . ' is not allowed');
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Low level method to set directives from an associative array
|
||||||
|
*
|
||||||
|
* @param array $directives
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setDirectivesFromArray($directives)
|
||||||
|
{
|
||||||
|
foreach ($directives as $directive => $value) {
|
||||||
|
// null values mean remove
|
||||||
|
if (is_null($value)) {
|
||||||
|
$this->removeDirective($directive);
|
||||||
|
} else {
|
||||||
|
// for legacy reasons we accept the string literal "true" as a bool
|
||||||
|
// a bool value of true means there is no explicit value for the directive
|
||||||
|
if ($value && (is_bool($value) || strtolower($value) === 'true')) {
|
||||||
|
$value = null;
|
||||||
|
}
|
||||||
|
$this->setDirective($directive, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Low level method for removing directives
|
||||||
|
*
|
||||||
|
* @param string $directive
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function removeDirective($directive)
|
||||||
|
{
|
||||||
|
unset($this->state[strtolower($directive)]);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Low level method to check if a directive is currently set
|
||||||
|
*
|
||||||
|
* @param string $directive
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasDirective($directive)
|
||||||
|
{
|
||||||
|
return array_key_exists(strtolower($directive), $this->state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Low level method to get the value of a directive
|
||||||
|
*
|
||||||
|
* Note that `null` value is acceptable for a directive
|
||||||
|
*
|
||||||
|
* @param string $directive
|
||||||
|
*
|
||||||
|
* @return string|false|null
|
||||||
|
*/
|
||||||
|
public function getDirective($directive)
|
||||||
|
{
|
||||||
|
if ($this->hasDirective($directive)) {
|
||||||
|
return $this->state[strtolower($directive)];
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cache should not store anything about the client request or server response.
|
||||||
|
*
|
||||||
|
* Set the no-store directive (also removes max-age and s-maxage for consistency purposes)
|
||||||
|
*
|
||||||
|
* @param bool $noStore
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setNoStore($noStore = true)
|
||||||
|
{
|
||||||
|
if ($noStore) {
|
||||||
|
$this->setDirective('no-store');
|
||||||
|
$this->removeDirective('max-age');
|
||||||
|
$this->removeDirective('s-maxage');
|
||||||
|
} else {
|
||||||
|
$this->removeDirective('no-store');
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces caches to submit the request to the origin server for validation before releasing a cached copy.
|
||||||
|
*
|
||||||
|
* @param bool $noCache
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setNoCache($noCache = true)
|
||||||
|
{
|
||||||
|
if ($noCache) {
|
||||||
|
$this->setDirective('no-cache');
|
||||||
|
} else {
|
||||||
|
$this->removeDirective('no-cache');
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the maximum amount of time (seconds) a resource will be considered fresh.
|
||||||
|
* This directive is relative to the time of the request.
|
||||||
|
*
|
||||||
|
* @param int $age
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setMaxAge($age)
|
||||||
|
{
|
||||||
|
$this->setDirective('max-age', $age);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides max-age or the Expires header, but it only applies to shared caches (e.g., proxies)
|
||||||
|
* and is ignored by a private cache.
|
||||||
|
*
|
||||||
|
* @param int $age
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setSharedMaxAge($age)
|
||||||
|
{
|
||||||
|
$this->setDirective('s-maxage', $age);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cache must verify the status of the stale resources before using it and expired ones should not be used.
|
||||||
|
*
|
||||||
|
* @param bool $mustRevalidate
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setMustRevalidate($mustRevalidate = true)
|
||||||
|
{
|
||||||
|
if ($mustRevalidate) {
|
||||||
|
$this->setDirective('must-revalidate');
|
||||||
|
} else {
|
||||||
|
$this->removeDirective('must-revalidate');
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple way to set cache control header to a cacheable state.
|
||||||
|
* Use this method over `publicCache()` if you are unsure about caching details.
|
||||||
|
*
|
||||||
|
* Removes `no-store` and `no-cache` directives; other directives will remain in place.
|
||||||
|
* Use alongside `setMaxAge()` to indicate caching.
|
||||||
|
*
|
||||||
|
* Does not set `public` directive. Usually, `setMaxAge()` is sufficient. Use `publicCache()` if this is explicitly required.
|
||||||
|
* See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private
|
||||||
|
*
|
||||||
|
* @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
|
||||||
|
* @param bool $force Force the cache to public even if its unforced private or public
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function enableCache($force = false)
|
||||||
|
{
|
||||||
|
// Only execute this if its forcing level is high enough
|
||||||
|
if (!$this->applyChangeLevel(self::LEVEL_ENABLED, $force)) {
|
||||||
|
SS_Log::log("Call to enableCache($force) didn't execute as it's lower priority than a previous call", SS_Log::DEBUG);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->removeDirective('no-store');
|
||||||
|
$this->removeDirective('no-cache');
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple way to set cache control header to a non-cacheable state.
|
||||||
|
* Use this method over `privateCache()` if you are unsure about caching details.
|
||||||
|
* Takes precendence over unforced `enableCache()`, `privateCache()` or `publicCache()` calls.
|
||||||
|
*
|
||||||
|
* Removes all state and replaces it with `no-cache, no-store, must-revalidate`. Although `no-store` is sufficient
|
||||||
|
* the others are added under recommendation from Mozilla (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Examples)
|
||||||
|
*
|
||||||
|
* Does not set `private` directive, use `privateCache()` if this is explicitly required.
|
||||||
|
* See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private
|
||||||
|
*
|
||||||
|
* @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
|
||||||
|
* @param bool $force Force the cache to diabled even if it's forced private or public
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function disableCache($force = false)
|
||||||
|
{
|
||||||
|
// Only execute this if its forcing level is high enough
|
||||||
|
if (!$this->applyChangeLevel(self::LEVEL_DISABLED, $force )) {
|
||||||
|
SS_Log::log("Call to disableCache($force) didn't execute as it's lower priority than a previous call", SS_Log::DEBUG);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state = array(
|
||||||
|
'no-cache' => null,
|
||||||
|
'no-store' => null,
|
||||||
|
'must-revalidate' => null,
|
||||||
|
);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced way to set cache control header to a non-cacheable state.
|
||||||
|
* Indicates that the response is intended for a single user and must not be stored by a shared cache.
|
||||||
|
* A private cache (e.g. Web Browser) may store the response. Also removes `public` as this is a contradictory directive.
|
||||||
|
*
|
||||||
|
* @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
|
||||||
|
* @param bool $force Force the cache to private even if it's forced public
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function privateCache($force = false)
|
||||||
|
{
|
||||||
|
// Only execute this if its forcing level is high enough
|
||||||
|
if (!$this->applyChangeLevel(self::LEVEL_PRIVATE, $force)) {
|
||||||
|
SS_Log::log("Call to privateCache($force) didn't execute as it's lower priority than a previous call", SS_Log::DEBUG);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the directives
|
||||||
|
$this->setDirective('private');
|
||||||
|
$this->removeDirective('public');
|
||||||
|
$this->removeDirective('no-cache');
|
||||||
|
$this->removeDirective('no-store');
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced way to set cache control header to a cacheable state.
|
||||||
|
* Indicates that the response may be cached by any cache. (eg: CDNs, Proxies, Web browsers)
|
||||||
|
* Also removes `private` as this is a contradictory directive
|
||||||
|
*
|
||||||
|
* @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
|
||||||
|
* @param bool $force Force the cache to public even if it's private, unless it's been forced private
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function publicCache($force = false)
|
||||||
|
{
|
||||||
|
// Only execute this if its forcing level is high enough
|
||||||
|
if (!$this->applyChangeLevel(self::LEVEL_PUBLIC, $force)) {
|
||||||
|
SS_Log::log("Call to publicCache($force) didn't execute as it's lower priority than a previous call", SS_Log::DEBUG);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setDirective('public');
|
||||||
|
$this->removeDirective('private');
|
||||||
|
$this->removeDirective('no-cache');
|
||||||
|
$this->removeDirective('no-store');
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate and add the `Cache-Control` header to a response object
|
||||||
|
*
|
||||||
|
* @param SS_HTTPResponse $response
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function applyToResponse($response)
|
||||||
|
{
|
||||||
|
$headers = $this->generateHeaders();
|
||||||
|
foreach ($headers as $name => $value) {
|
||||||
|
$response->addHeader($name, $value);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the cache header
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function generateCacheHeader()
|
||||||
|
{
|
||||||
|
$cacheControl = array();
|
||||||
|
foreach ($this->state as $directive => $value) {
|
||||||
|
if (is_null($value)) {
|
||||||
|
$cacheControl[] = $directive;
|
||||||
|
} else {
|
||||||
|
$cacheControl[] = $directive . '=' . $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return implode(', ', $cacheControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate all headers to output
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function generateHeaders()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'Cache-Control' => $this->generateCacheHeader(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset registered http cache control and force a fresh instance to be built
|
||||||
|
*/
|
||||||
|
public static function reset() {
|
||||||
|
Injector::inst()->unregisterNamedObject(__CLASS__);
|
||||||
|
}
|
||||||
|
}
|
@ -189,9 +189,15 @@ class SS_HTTPResponse {
|
|||||||
* @param string $header
|
* @param string $header
|
||||||
* @returns null|string
|
* @returns null|string
|
||||||
*/
|
*/
|
||||||
public function getHeader($header) {
|
public function getHeader($header, $anyCase = false) {
|
||||||
if(isset($this->headers[$header]))
|
if ($anyCase) {
|
||||||
return $this->headers[$header];
|
$headers = array_change_key_case($this->headers, CASE_LOWER);
|
||||||
|
$header = strtolower($header);
|
||||||
|
} else {
|
||||||
|
$headers = $this->headers;
|
||||||
|
}
|
||||||
|
if(isset($headers[$header]))
|
||||||
|
return $headers[$header];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,6 +34,7 @@ class VersionedRequestFilter implements RequestFilter {
|
|||||||
if(class_exists('SapphireTest', false) && SapphireTest::is_running_test()) {
|
if(class_exists('SapphireTest', false) && SapphireTest::is_running_test()) {
|
||||||
throw new SS_HTTPResponse_Exception($response);
|
throw new SS_HTTPResponse_Exception($response);
|
||||||
}
|
}
|
||||||
|
HTTP::add_cache_headers($response);
|
||||||
$response->output();
|
$response->output();
|
||||||
die;
|
die;
|
||||||
}
|
}
|
||||||
@ -44,6 +45,9 @@ class VersionedRequestFilter implements RequestFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function postRequest(SS_HTTPRequest $request, SS_HTTPResponse $response, DataModel $model) {
|
public function postRequest(SS_HTTPRequest $request, SS_HTTPResponse $response, DataModel $model) {
|
||||||
|
if (Versioned::current_stage() !== Versioned::LIVE && !HTTPCacheControl::singleton()->hasDirective('no-store')) {
|
||||||
|
HTTPCacheControl::singleton()->privateCache(true);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,8 +131,8 @@ class SS_Log {
|
|||||||
/**
|
/**
|
||||||
* Add a writer instance to the logger.
|
* Add a writer instance to the logger.
|
||||||
* @param object $writer Zend_Log_Writer_Abstract instance
|
* @param object $writer Zend_Log_Writer_Abstract instance
|
||||||
* @param const $priority Priority. Possible values: SS_Log::ERR, SS_Log::WARN or SS_Log::NOTICE
|
* @param int $priority Priority. Possible values: SS_Log::ERR, SS_Log::WARN or SS_Log::NOTICE
|
||||||
* @param $comparison Priority comparison operator. Acts on the integer values of the error
|
* @param string $comparison Priority comparison operator. Acts on the integer values of the error
|
||||||
* levels, where more serious errors are lower numbers. By default this is "=", which means only
|
* levels, where more serious errors are lower numbers. By default this is "=", which means only
|
||||||
* the given priority will be logged. Set to "<=" if you want to track errors of *at least*
|
* the given priority will be logged. Set to "<=" if you want to track errors of *at least*
|
||||||
* the given priority.
|
* the given priority.
|
||||||
@ -151,7 +151,7 @@ class SS_Log {
|
|||||||
* error code, error line, error context (backtrace).
|
* error code, error line, error context (backtrace).
|
||||||
*
|
*
|
||||||
* @param mixed $message Exception object or array of error context variables
|
* @param mixed $message Exception object or array of error context variables
|
||||||
* @param const $priority Priority. Possible values: SS_Log::ERR, SS_Log::WARN or SS_Log::NOTICE
|
* @param int $priority Priority. Possible values: SS_Log::ERR, SS_Log::WARN or SS_Log::NOTICE
|
||||||
* @param mixed $extras Extra information to log in event
|
* @param mixed $extras Extra information to log in event
|
||||||
*/
|
*/
|
||||||
public static function log($message, $priority, $extras = null) {
|
public static function log($message, $priority, $extras = null) {
|
||||||
|
@ -65,7 +65,18 @@ application errors or edge cases.
|
|||||||
SilverStripe has no built-in protection for detailing with bots, captcha or other spam protection methods. This
|
SilverStripe has no built-in protection for detailing with bots, captcha or other spam protection methods. This
|
||||||
functionality is available as an additional [Spam Protection](https://github.com/silverstripe/silverstripe-spamprotection)
|
functionality is available as an additional [Spam Protection](https://github.com/silverstripe/silverstripe-spamprotection)
|
||||||
module if required. The module provides an consistent API for allowing third-party spam protection handlers such as
|
module if required. The module provides an consistent API for allowing third-party spam protection handlers such as
|
||||||
[Recaptcha](http://www.google.com/recaptcha/intro/) and [Mollom](https://mollom.com/) to work within the `Form` API.
|
[Recaptcha](http://www.google.com/recaptcha/intro/) and [Mollom](https://mollom.com/) to work within the `Form` API.
|
||||||
|
|
||||||
|
## Data disclosure through HTTP Caching (since 3.7.0)
|
||||||
|
|
||||||
|
Forms, and particularly their responses, can contain sensitive data (such as when data is pre-populated or re-posted due
|
||||||
|
to validation errors). This data can inadvertently be stored either in a user's browser cache or in an intermediary
|
||||||
|
cache such as a CDN or other caching-proxy. If incorrect `Cache-Conrol` headers are used, private data may be cached and
|
||||||
|
accessible publicly through the CDN.
|
||||||
|
|
||||||
|
To ensure this doesn't happen SilverStripe adds `Cache-Control: no-store, no-cache, must-revalidate` headers to any
|
||||||
|
forms that have validators or security tokens (all of them by default) applied to them; this ensures that CDNs
|
||||||
|
(and browsers) will not cache these pages.
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
@ -73,4 +84,4 @@ module if required. The module provides an consistent API for allowing third-par
|
|||||||
|
|
||||||
## API Documentation
|
## API Documentation
|
||||||
|
|
||||||
* [api:SecurityToken]
|
* [api:SecurityToken]
|
||||||
|
@ -1,7 +1,175 @@
|
|||||||
title: HTTP Cache Headers
|
title: HTTP Cache Headers
|
||||||
summary: Set the correct HTTP cache headers for your responses.
|
summary: Set the correct HTTP cache headers for your responses.
|
||||||
|
|
||||||
# Caching Headers
|
# HTTP Cache Headers
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
By default, SilverStripe sends headers which signal to HTTP caches
|
||||||
|
that the response should be not considered cacheable.
|
||||||
|
HTTP caches can either be intermediary caches (e.g. CDNs and proxies), or clients (e.g. browsers).
|
||||||
|
The cache headers sent are `Cache-Control: no-store, no-cache, must-revalidate`;
|
||||||
|
|
||||||
|
HTTP caching can be a great way to speed up your website, but needs to be properly applied.
|
||||||
|
Getting it wrong can accidentally expose draft pages or other protected content.
|
||||||
|
The [Google Web Fundamentals](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private)
|
||||||
|
are a great way to learn about HTTP caching.
|
||||||
|
|
||||||
|
## Cache Control Headers
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
In order to support developers in making safe choices around HTTP caching,
|
||||||
|
we're using a `HTTPCacheControl` class to control if a response
|
||||||
|
should be considered public or private. This is an abstraction on existing
|
||||||
|
lowlevel APIs like `HTTP::add_cache_headers()` and `SS_HTTPResponse->addHeader()`.
|
||||||
|
|
||||||
|
The `HTTPCacheControl` API makes it easier to express your caching preferences
|
||||||
|
without running the risk of overriding essential core safety measures.
|
||||||
|
Most commonly, these APIs will prevent HTTP caching of draft content.
|
||||||
|
|
||||||
|
It will also prevent caching of content generated with an active session,
|
||||||
|
since the system can't tell whether session data was used to vary the output.
|
||||||
|
In this case, it's up to the developer to opt-in to caching,
|
||||||
|
after ensuring that certain execution paths are safe despite of using sessions.
|
||||||
|
|
||||||
|
The system behaviour does not guard against accidentally caching "private" content,
|
||||||
|
since there are too many variations under which output could be considered private
|
||||||
|
(e.g. a custom "approval" flag on a comment object). It is up to
|
||||||
|
the developer to ensure caching is used appropriately there.
|
||||||
|
|
||||||
|
The `HTTPCacheControl` class supplements the `HTTP` helper class.
|
||||||
|
It comes with methods which let developers safely interact with the `Cache-Control` header.
|
||||||
|
|
||||||
|
### disableCache()
|
||||||
|
|
||||||
|
Simple way to set cache control header to a non-cacheable state.
|
||||||
|
Use this method over `privateCache()` if you are unsure about caching details.
|
||||||
|
Takes precendence over unforced `enableCache()`, `privateCache()` or `publicCache()` calls.
|
||||||
|
|
||||||
|
Removes all state and replaces it with `no-cache, no-store, must-revalidate`. Although `no-store` is sufficient
|
||||||
|
the others are added under [recommendation from Mozilla](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Examples)
|
||||||
|
|
||||||
|
Does not set `private` directive, use `privateCache()` if this is explicitly required
|
||||||
|
([details](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private))
|
||||||
|
|
||||||
|
### enableCache()
|
||||||
|
|
||||||
|
Simple way to set cache control header to a cacheable state.
|
||||||
|
Use this method over `publicCache()` if you are unsure about caching details.
|
||||||
|
|
||||||
|
Removes `no-store` and `no-cache` directives; other directives will remain in place.
|
||||||
|
Use alongside `setMaxAge()` to indicate caching.
|
||||||
|
|
||||||
|
Does not set `public` directive. Usually, `setMaxAge()` is sufficient. Use `publicCache()` if this is explicitly required
|
||||||
|
([details](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private))
|
||||||
|
|
||||||
|
### privateCache()
|
||||||
|
|
||||||
|
Advanced way to set cache control header to a non-cacheable state.
|
||||||
|
Indicates that the response is intended for a single user and must not be stored by a shared cache.
|
||||||
|
A private cache (e.g. Web Browser) may store the response. Also removes `public` as this is a contradictory directive.
|
||||||
|
|
||||||
|
### publicCache()
|
||||||
|
|
||||||
|
Advanced way to set cache control header to a cacheable state.
|
||||||
|
Indicates that the response may be cached by any cache. (eg: CDNs, Proxies, Web browsers)
|
||||||
|
Also removes `private` as this is a contradictory directive
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
|
||||||
|
Each of these highlevel methods has a boolean `$force` parameter which determines
|
||||||
|
their application priority regardless of execution order.
|
||||||
|
The priority order is as followed, sorted in descending order
|
||||||
|
(earlier items will overrule later items):
|
||||||
|
|
||||||
|
* `disableCache($force=true)`
|
||||||
|
* `privateCache($force=true)`
|
||||||
|
* `publicCache($force=true)`
|
||||||
|
* `enableCache($force=true)`
|
||||||
|
* `disableCache()`
|
||||||
|
* `privateCache()`
|
||||||
|
* `publicCache()`
|
||||||
|
* `enableCache()`
|
||||||
|
|
||||||
|
## Cache Control Examples
|
||||||
|
|
||||||
|
### Global opt-in for page content
|
||||||
|
|
||||||
|
Enable caching for all page content (through `Page_Controller`).
|
||||||
|
|
||||||
|
```php
|
||||||
|
class Page_Controller extends ContentController
|
||||||
|
{
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
HTTPCacheControl::inst()
|
||||||
|
->enableCache()
|
||||||
|
->setMaxAge(60); // 1 minute
|
||||||
|
|
||||||
|
|
||||||
|
parent::init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: SilverStripe will still override this preference when a session is active,
|
||||||
|
a [CSRF token](/developer_guides/forms/form_security) token is present,
|
||||||
|
or draft content has been requested.
|
||||||
|
|
||||||
|
### Opt-out for a particular controller action
|
||||||
|
|
||||||
|
If a controller output relies on session data, cookies,
|
||||||
|
permission checks or other triggers for conditional output,
|
||||||
|
you can disable caching either on a controller level
|
||||||
|
(through `init()`) or for a particular action.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class MyPage_Controller extends Page_Controller
|
||||||
|
{
|
||||||
|
public function myprivateaction($request)
|
||||||
|
{
|
||||||
|
$response = $this->myPrivateResponse();
|
||||||
|
HTTPCacheControl::inst()
|
||||||
|
->disableCache();
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: SilverStripe will still override this preference when a session is active,
|
||||||
|
a [CSRF token](/developer_guides/forms/form_security) token is present,
|
||||||
|
or draft content has been requested.
|
||||||
|
|
||||||
|
### Global opt-in, ignoring session (advanced)
|
||||||
|
|
||||||
|
This can be helpful in situations where forms are embedded on the website.
|
||||||
|
SilverStripe will still override this preference when draft content has been requested.
|
||||||
|
CAUTION: This mode relies on a developer examining each execution path to ensure
|
||||||
|
that no session data is used to vary output.
|
||||||
|
|
||||||
|
Use case: By default, forms include a [CSRF token](/developer_guides/forms/form_security)
|
||||||
|
which starts a session with a value that's unique to the visitor, which makes the output uncacheable.
|
||||||
|
But any subsequent requests by this visitor will also carry a session, leading to uncacheable output
|
||||||
|
for this visitor. This is the case even if the output does not contain any forms,
|
||||||
|
and does not vary for this particular visitor.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class Page_Controller extends ContentController
|
||||||
|
{
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
HTTPCacheControl::inst()
|
||||||
|
->enableCache($force=true) // DANGER ZONE
|
||||||
|
->setMaxAge(60); // 1 minute
|
||||||
|
|
||||||
|
parent::init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Defaults
|
||||||
|
|
||||||
By default, PHP adds caching headers that make the page appear purely dynamic. This isn't usually appropriate for most
|
By default, PHP adds caching headers that make the page appear purely dynamic. This isn't usually appropriate for most
|
||||||
sites, even ones that are updated reasonably frequently. SilverStripe overrides the default settings with the following
|
sites, even ones that are updated reasonably frequently. SilverStripe overrides the default settings with the following
|
||||||
@ -14,40 +182,39 @@ headers:
|
|||||||
* Since a visitor cookie is set, the site won't be cached by proxies.
|
* Since a visitor cookie is set, the site won't be cached by proxies.
|
||||||
* Ajax requests are never cached.
|
* Ajax requests are never cached.
|
||||||
|
|
||||||
## Customizing Cache Headers
|
## Max Age
|
||||||
|
|
||||||
### HTTP::set_cache_age
|
The cache age determines the lifetime of your cache, in seconds.
|
||||||
|
It only takes effect if you instruct the cache control
|
||||||
:::php
|
that your response is public in the first place (via `enableCache()` or via modifying the `HTTP.cache_control` defaults).
|
||||||
HTTP::set_cache_age(0);
|
|
||||||
|
|
||||||
Used to set the max-age component of the cache-control line, in seconds. Set it to 0 to disable caching; the "no-cache"
|
|
||||||
clause in `Cache-Control` and `Pragma` will be included.
|
|
||||||
|
|
||||||
### HTTP::register_modification_date
|
|
||||||
|
|
||||||
:::php
|
:::php
|
||||||
HTTP::register_modification_date('2014-10-10');
|
HTTPCacheControl::singleton()
|
||||||
|
->setMaxAge(60)
|
||||||
|
|
||||||
|
Note that `setMaxAge(0)` is NOT sufficient to disable caching in all cases.
|
||||||
|
|
||||||
|
### Last Modified
|
||||||
|
|
||||||
Used to set the modification date to something more recent than the default. [api:DataObject::__construct] calls
|
Used to set the modification date to something more recent than the default. [api:DataObject::__construct] calls
|
||||||
[api:HTTP::register_modification_date(] whenever a record comes from the database ensuring the newest date is present.
|
[api:HTTP::register_modification_date(] whenever a record comes from the database ensuring the newest date is present.
|
||||||
|
|
||||||
### Vary: cache header
|
:::php
|
||||||
|
HTTP::register_modification_date('2014-10-10');
|
||||||
|
|
||||||
By default, SilverStripe will output a `Vary` header (used by upstream caches for determining uniqueness)
|
### Vary
|
||||||
that looks like
|
|
||||||
|
A `Vary` header tells caches which aspects of the response should be considered
|
||||||
|
when calculating a cache key, usually in addition to the full URL path.
|
||||||
|
By default, SilverStripe will output a `Vary` header with the following content:
|
||||||
|
|
||||||
```
|
```
|
||||||
Cookie, X-Forwarded-Protocol, User-Agent, Accept
|
Vary: X-Requested-With, X-Forwarded-Protocol
|
||||||
```
|
```
|
||||||
|
|
||||||
To change the value of the `Vary` header, you can change this value by specifying the header in configuration
|
To change the value of the `Vary` header, you can change this value by specifying the header in configuration.
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
HTTP:
|
HTTP:
|
||||||
vary: ""
|
vary: ""
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -607,6 +607,15 @@ In a future release this behaviour will be changed to be on by default, and this
|
|||||||
variable will be no longer necessary, thus it will be necessary to always set
|
variable will be no longer necessary, thus it will be necessary to always set
|
||||||
SS_TRUSTED_PROXY_IPS if using a proxy.
|
SS_TRUSTED_PROXY_IPS if using a proxy.
|
||||||
|
|
||||||
|
## HTTP Caching Headers
|
||||||
|
|
||||||
|
Caching is hard. If you get it wrong, private or draft content might leak
|
||||||
|
to unauthenticated users. We have created an abstraction which allows you to express
|
||||||
|
your intent around HTTP caching without worrying too much about the details.
|
||||||
|
See [/developer_guides/performances/http_cache_headers](Developer Guides > Performance > HTTP Cache Headers)
|
||||||
|
for details on how to apply caching safely, and read Google's
|
||||||
|
[Web Fundamentals on Caching](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching).
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
* [http://silverstripe.org/security-releases/](http://silverstripe.org/security-releases/)
|
* [http://silverstripe.org/security-releases/](http://silverstripe.org/security-releases/)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# 3.7.0 (unreleased)
|
# 3.7.0
|
||||||
|
|
||||||
## SilverStripe 3.7 and PHP 7.2 and Object subclasses
|
## SilverStripe 3.7 and PHP 7.2 and Object subclasses
|
||||||
|
|
||||||
@ -67,3 +67,132 @@ Data that is not content sensitive can be cached across stages by simply opting
|
|||||||
$cache = SS_Cache::factory('myapp', 'Output', array('disable-segmentation' => true));
|
$cache = SS_Cache::factory('myapp', 'Output', array('disable-segmentation' => true));
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## HTTP Cache Header changes
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
In order to support developers in making safe choices around HTTP caching,
|
||||||
|
we've introduced a `HTTPCacheControl` class to control if a response
|
||||||
|
should be considered public or private. This is an abstraction on existing
|
||||||
|
lowlevel APIs like `HTTP::add_cache_headers()` and `SS_HTTPResponse->addHeader()`.
|
||||||
|
|
||||||
|
This change introduces smaller but necessary changes to HTTP caching headers
|
||||||
|
sent by SilverStripe. If you are relying on HTTP caching in your implementation,
|
||||||
|
or use modules such as [silverstripe/controllerpolicy](https://github.com/silverstripe/silverstripe-controllerpolicy),
|
||||||
|
please review the implications of these changes below.
|
||||||
|
|
||||||
|
In short, these APIs make it easier to express your caching preferences
|
||||||
|
without running the risk of overriding essential core safety measures.
|
||||||
|
Most commonly, these APIs will prevent HTTP caching of draft content.
|
||||||
|
|
||||||
|
It will also prevent caching of content generated with an active session,
|
||||||
|
since the system can't tell whether session data was used to vary the output.
|
||||||
|
In this case, it's up to the developer to opt-in to caching,
|
||||||
|
after ensuring that certain execution paths are safe despite of using sessions.
|
||||||
|
|
||||||
|
The system behaviour does not guard against accidentally caching "private" content,
|
||||||
|
since there are too many variations under which output could be considered private
|
||||||
|
(e.g. a custom "approval" flag on a comment object). It is up to
|
||||||
|
the developer to ensure caching is used appropriately there.
|
||||||
|
|
||||||
|
By default, SilverStripe sends headers which signal to HTTP caches
|
||||||
|
that the response should be considered not cacheable.
|
||||||
|
|
||||||
|
See [Developer Guide: Performance > HTTP Cache Headers](/developer_guide/performance/http_cache_headers)
|
||||||
|
for details on the new API.
|
||||||
|
|
||||||
|
### Example Usage
|
||||||
|
|
||||||
|
#### Global opt-in for page content
|
||||||
|
|
||||||
|
Enable caching for all page content (through `Page_Controller`).
|
||||||
|
|
||||||
|
```diff
|
||||||
|
class Page_Controller extends ContentController
|
||||||
|
{
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
- HTTP::set_cache_age(60);
|
||||||
|
+ HTTPCacheControl::inst()
|
||||||
|
+ ->enableCache()
|
||||||
|
+ ->setMaxAge(60); // 1 minute
|
||||||
|
|
||||||
|
|
||||||
|
parent::init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: SilverStripe will still override this preference when a session is active,
|
||||||
|
a [CSRF token](/developer_guides/forms/form_security) token is present,
|
||||||
|
or draft content has been requested.
|
||||||
|
|
||||||
|
#### Opt-out for a particular controller action
|
||||||
|
|
||||||
|
If a controller output relies on session data, cookies,
|
||||||
|
permission checks or other triggers for conditional output,
|
||||||
|
you can disable caching either on a controller level
|
||||||
|
(through `init()`) or for a particular action.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
class MyPage_Controller extends Page_Controller
|
||||||
|
{
|
||||||
|
public function myprivateaction($request)
|
||||||
|
{
|
||||||
|
$response = $this->myPrivateResponse();
|
||||||
|
- HTTP::set_cache_age(0);
|
||||||
|
+ HTTPCacheControl::inst()
|
||||||
|
+ ->disableCache();
|
||||||
|
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: SilverStripe will still override this preference when a session is active,
|
||||||
|
a [CSRF token](/developer_guides/forms/form_security) token is present,
|
||||||
|
or draft content has been requested.
|
||||||
|
|
||||||
|
#### Global opt-in, ignoring session (advanced)
|
||||||
|
|
||||||
|
This can be helpful in situations where forms are embedded on the website.
|
||||||
|
SilverStripe will still override this preference when draft content has been requested.
|
||||||
|
CAUTION: This mode relies on a developer examining each execution path to ensure
|
||||||
|
that no session data is used to vary output.
|
||||||
|
|
||||||
|
Use case: By default, forms include a [CSRF token](/developer_guides/forms/form_security)
|
||||||
|
which starts a session with a value that's unique to the visitor, which makes the output uncacheable.
|
||||||
|
But any subsequent requests by this visitor will also carry a session, leading to uncacheable output
|
||||||
|
for this visitor. This is the case even if the output does not contain any forms,
|
||||||
|
and does not vary for this particular visitor.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
class Page_Controller extends ContentController
|
||||||
|
{
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
- HTTP::set_cache_age(60);
|
||||||
|
+ HTTPCacheControl::inst()
|
||||||
|
+ ->enableCache($force=true) // DANGER ZONE
|
||||||
|
+ ->setMaxAge(60); // 1 minute
|
||||||
|
|
||||||
|
|
||||||
|
parent::init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detailed Changes
|
||||||
|
|
||||||
|
* Added `Cache-Control: no-store` header to default responses,
|
||||||
|
to prevent intermediary HTTP proxies (e.g. CDNs) from caching unless developers opt-in
|
||||||
|
* Removed `Cache-Control: no-transform` header from default responses
|
||||||
|
* Removed `Vary: Cookie` as an unreliable cache buster,
|
||||||
|
rely on the existing `Cache-Control: no-store` defaults instead
|
||||||
|
* Removed `Vary: Accept`, since it's very uncommon to vary content on
|
||||||
|
the `Content-Type` headers submitted through the request,
|
||||||
|
and it can significantly decrease the likelyhood of a cache hit.
|
||||||
|
Note this is different from `Vary: Accept-Encoding`,
|
||||||
|
which is important for compression (e.g. gzip), and usually added by
|
||||||
|
other layers such as Apache's mod_gzip.
|
||||||
|
@ -854,7 +854,9 @@ class Form extends RequestHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we need to disable cache, do it
|
// If we need to disable cache, do it
|
||||||
if ($needsCacheDisabled) HTTP::set_cache_age(0);
|
if ($needsCacheDisabled) {
|
||||||
|
HTTPCacheControl::singleton()->disableCache(true);
|
||||||
|
}
|
||||||
|
|
||||||
$attrs = $this->getAttributes();
|
$attrs = $this->getAttributes();
|
||||||
|
|
||||||
|
3
main.php
3
main.php
@ -57,6 +57,9 @@ if (version_compare(phpversion(), '5.3.3', '<')) {
|
|||||||
*/
|
*/
|
||||||
require_once('core/Constants.php');
|
require_once('core/Constants.php');
|
||||||
|
|
||||||
|
// we handle our own cache headers in this application
|
||||||
|
session_cache_limiter('');
|
||||||
|
|
||||||
// IIS will sometimes generate this.
|
// IIS will sometimes generate this.
|
||||||
if(!empty($_SERVER['HTTP_X_ORIGINAL_URL'])) {
|
if(!empty($_SERVER['HTTP_X_ORIGINAL_URL'])) {
|
||||||
$_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_ORIGINAL_URL'];
|
$_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_ORIGINAL_URL'];
|
||||||
|
@ -240,6 +240,8 @@ class Security extends Controller implements TemplateGlobalProvider {
|
|||||||
|
|
||||||
if(!$controller) $controller = Controller::curr();
|
if(!$controller) $controller = Controller::curr();
|
||||||
|
|
||||||
|
HTTPCacheControl::singleton()->disableCache(true);
|
||||||
|
|
||||||
if(Director::is_ajax()) {
|
if(Director::is_ajax()) {
|
||||||
$response = ($controller) ? $controller->getResponse() : new SS_HTTPResponse();
|
$response = ($controller) ? $controller->getResponse() : new SS_HTTPResponse();
|
||||||
$response->setStatusCode(403);
|
$response->setStatusCode(403);
|
||||||
|
190
tests/control/HTTPCacheControlIntegrationTest.php
Normal file
190
tests/control/HTTPCacheControlIntegrationTest.php
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class HTTPCacheControlIntegrationTest extends FunctionalTest {
|
||||||
|
|
||||||
|
public function setUp() {
|
||||||
|
parent::setUp();
|
||||||
|
Config::inst()->remove('HTTP', 'disable_http_cache');
|
||||||
|
HTTPCacheControl::reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormCSRF() {
|
||||||
|
// CSRF sets caching to disabled
|
||||||
|
$response = $this->get('HTTPCacheControlIntegrationTest_SessionController/showform');
|
||||||
|
$header = $response->getHeader('Cache-Control');
|
||||||
|
$this->assertFalse($response->isError());
|
||||||
|
$this->assertNotContains('public', $header);
|
||||||
|
$this->assertNotContains('private', $header);
|
||||||
|
$this->assertContains('no-cache', $header);
|
||||||
|
$this->assertContains('no-store', $header);
|
||||||
|
$this->assertContains('must-revalidate', $header);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPublicForm() {
|
||||||
|
// Public forms (http get) allow public caching
|
||||||
|
$response = $this->get('HTTPCacheControlIntegrationTest_SessionController/showpublicform');
|
||||||
|
$header = $response->getHeader('Cache-Control');
|
||||||
|
$this->assertFalse($response->isError());
|
||||||
|
$this->assertContains('public', $header);
|
||||||
|
$this->assertContains('must-revalidate', $header);
|
||||||
|
$this->assertNotContains('no-cache', $response->getHeader('Cache-Control'));
|
||||||
|
$this->assertNotContains('no-store', $response->getHeader('Cache-Control'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPrivateActionsError()
|
||||||
|
{
|
||||||
|
// disallowed private actions don't cache
|
||||||
|
$response = $this->get('HTTPCacheControlIntegrationTest_SessionController/privateaction');
|
||||||
|
$header = $response->getHeader('Cache-Control');
|
||||||
|
$this->assertTrue($response->isError());
|
||||||
|
$this->assertContains('no-cache', $header);
|
||||||
|
$this->assertContains('no-store', $header);
|
||||||
|
$this->assertContains('must-revalidate', $header);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPrivateActionsAuthenticated()
|
||||||
|
{
|
||||||
|
$this->logInWithPermission('ADMIN');
|
||||||
|
// Authenticated actions are private cache
|
||||||
|
$response = $this->get('HTTPCacheControlIntegrationTest_SessionController/privateaction');
|
||||||
|
$header = $response->getHeader('Cache-Control');
|
||||||
|
$this->assertFalse($response->isError());
|
||||||
|
$this->assertContains('private', $header);
|
||||||
|
$this->assertContains('must-revalidate', $header);
|
||||||
|
$this->assertNotContains('no-cache', $header);
|
||||||
|
$this->assertNotContains('no-store', $header);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPrivateCache() {
|
||||||
|
$response = $this->get('HTTPCacheControlIntegrationTest_RuleController/privateaction');
|
||||||
|
$header = $response->getHeader('Cache-Control');
|
||||||
|
$this->assertFalse($response->isError());
|
||||||
|
$this->assertContains('private', $header);
|
||||||
|
$this->assertContains('must-revalidate', $header);
|
||||||
|
$this->assertNotContains('no-cache', $header);
|
||||||
|
$this->assertNotContains('no-store', $header);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPublicCache() {
|
||||||
|
$response = $this->get('HTTPCacheControlIntegrationTest_RuleController/publicaction');
|
||||||
|
$header = $response->getHeader('Cache-Control');
|
||||||
|
$this->assertFalse($response->isError());
|
||||||
|
$this->assertContains('public', $header);
|
||||||
|
$this->assertContains('must-revalidate', $header);
|
||||||
|
$this->assertNotContains('no-cache', $header);
|
||||||
|
$this->assertNotContains('no-store', $header);
|
||||||
|
$this->assertContains('max-age=9000', $header);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDisabledCache() {
|
||||||
|
$response = $this->get('HTTPCacheControlIntegrationTest_RuleController/disabledaction');
|
||||||
|
$header = $response->getHeader('Cache-Control');
|
||||||
|
$this->assertFalse($response->isError());
|
||||||
|
$this->assertNotContains('public', $header);
|
||||||
|
$this->assertNotContains('private', $header);
|
||||||
|
$this->assertContains('no-cache', $header);
|
||||||
|
$this->assertContains('no-store', $header);
|
||||||
|
$this->assertContains('must-revalidate', $header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test caching based on session
|
||||||
|
*/
|
||||||
|
class HTTPCacheControlIntegrationTest_SessionController extends Controller implements TestOnly
|
||||||
|
{
|
||||||
|
private static $allowed_actions = array(
|
||||||
|
'showform',
|
||||||
|
'privateaction',
|
||||||
|
'publicaction',
|
||||||
|
'showpublicform',
|
||||||
|
'Form',
|
||||||
|
);
|
||||||
|
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
parent::init();
|
||||||
|
// Prefer public by default
|
||||||
|
HTTPCacheControl::singleton()->publicCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContent()
|
||||||
|
{
|
||||||
|
return '<p>Hello world</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showform()
|
||||||
|
{
|
||||||
|
// Form should be set to private due to CSRF
|
||||||
|
SecurityToken::enable();
|
||||||
|
return $this->renderWith('BlankPage');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showpublicform()
|
||||||
|
{
|
||||||
|
// Public form doesn't use CSRF and thus no session usage
|
||||||
|
SecurityToken::disable();
|
||||||
|
return $this->renderWith('BlankPage');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function privateaction()
|
||||||
|
{
|
||||||
|
if (!Permission::check('ANYCODE')) {
|
||||||
|
$this->httpError(403, 'Not allowed');
|
||||||
|
}
|
||||||
|
return 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function publicaction()
|
||||||
|
{
|
||||||
|
return 'Hello!';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Form()
|
||||||
|
{
|
||||||
|
$form = new Form(
|
||||||
|
$this,
|
||||||
|
'Form',
|
||||||
|
new FieldList(new TextField('Name')),
|
||||||
|
new FieldList(new FormAction('submit', 'Submit'))
|
||||||
|
);
|
||||||
|
$form->setFormMethod('GET');
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test caching based on specific http caching directives
|
||||||
|
*/
|
||||||
|
class HTTPCacheControlIntegrationTest_RuleController extends Controller implements TestOnly
|
||||||
|
{
|
||||||
|
private static $allowed_actions = array(
|
||||||
|
'privateaction',
|
||||||
|
'publicaction',
|
||||||
|
'disabledaction',
|
||||||
|
);
|
||||||
|
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
parent::init();
|
||||||
|
// Prefer public by default
|
||||||
|
HTTPCacheControl::singleton()->publicCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function privateaction() {
|
||||||
|
HTTPCacheControl::singleton()->privateCache();
|
||||||
|
return 'private content';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function publicaction() {
|
||||||
|
HTTPCacheControl::singleton()
|
||||||
|
->publicCache()
|
||||||
|
->setMaxAge(9000);
|
||||||
|
return 'public content';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disabledaction() {
|
||||||
|
HTTPCacheControl::singleton()->disableCache();
|
||||||
|
return 'uncached content';
|
||||||
|
}
|
||||||
|
}
|
66
tests/control/HTTPCacheControlTest.php
Normal file
66
tests/control/HTTPCacheControlTest.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testing of HTTPCacheControl class
|
||||||
|
*/
|
||||||
|
class HTTPCacheControlTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public function testCachingPriorities()
|
||||||
|
{
|
||||||
|
$hcc = new HTTPCacheControl();
|
||||||
|
$this->assertTrue($this->isDisabled($hcc), 'caching starts as disabled');
|
||||||
|
|
||||||
|
$hcc->enableCache();
|
||||||
|
$this->assertFalse($this->isDisabled($hcc));
|
||||||
|
|
||||||
|
$hcc->publicCache();
|
||||||
|
$this->assertTrue($this->isPublic($hcc), 'public can be set at start');
|
||||||
|
|
||||||
|
$hcc->privateCache();
|
||||||
|
$this->assertTrue($this->isPrivate($hcc), 'private overrides public');
|
||||||
|
|
||||||
|
$hcc->publicCache();
|
||||||
|
$this->assertFalse($this->isPublic($hcc), 'public does not overrides private');
|
||||||
|
|
||||||
|
$hcc->disableCache();
|
||||||
|
$this->assertTrue($this->isDisabled($hcc), 'disabled overrides private');
|
||||||
|
|
||||||
|
$hcc->privateCache();
|
||||||
|
$this->assertFalse($this->isPrivate($hcc), 'private does not override disabled');
|
||||||
|
|
||||||
|
$hcc->enableCache(true);
|
||||||
|
$this->assertFalse($this->isDisabled($hcc));
|
||||||
|
|
||||||
|
$hcc->publicCache(true);
|
||||||
|
$this->assertTrue($this->isPublic($hcc), 'force-public overrides disabled');
|
||||||
|
|
||||||
|
$hcc->privateCache();
|
||||||
|
$this->assertFalse($this->isPrivate($hcc), 'private does not overrdie force-public');
|
||||||
|
|
||||||
|
$hcc->privateCache(true);
|
||||||
|
$this->assertTrue($this->isPrivate($hcc), 'force-private overrides force-public');
|
||||||
|
|
||||||
|
$hcc->publicCache(true);
|
||||||
|
$this->assertFalse($this->isPublic($hcc), 'force-public does not override force-private');
|
||||||
|
|
||||||
|
$hcc->disableCache(true);
|
||||||
|
$this->assertTrue($this->isDisabled($hcc), 'force-disabled overrides force-private');
|
||||||
|
|
||||||
|
$hcc->publicCache(true);
|
||||||
|
$this->assertFalse($this->isPublic($hcc), 'force-public does not overrides force-disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isPrivate(HTTPCacheControl $hcc)
|
||||||
|
{
|
||||||
|
return $hcc->hasDirective('private') && !$hcc->hasDirective('public') && !$hcc->hasDirective('no-cache');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isPublic(HTTPCacheControl $hcc)
|
||||||
|
{
|
||||||
|
return $hcc->hasDirective('public') && !$hcc->hasDirective('private') && !$hcc->hasDirective('no-cache');
|
||||||
|
}
|
||||||
|
protected function isDisabled(HTTPCacheControl $hcc)
|
||||||
|
{
|
||||||
|
return $hcc->hasDirective('no-cache') && !$hcc->hasDirective('private') && !$hcc->hasDirective('public');
|
||||||
|
}
|
||||||
|
}
|
@ -7,27 +7,41 @@
|
|||||||
*/
|
*/
|
||||||
class HTTPTest extends FunctionalTest {
|
class HTTPTest extends FunctionalTest {
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
// Remove dev-only config
|
||||||
|
Config::inst()->remove('HTTP', 'disable_http_cache');
|
||||||
|
HTTPCacheControl::reset();
|
||||||
|
}
|
||||||
|
|
||||||
public function testAddCacheHeaders() {
|
public function testAddCacheHeaders() {
|
||||||
$body = "<html><head></head><body><h1>Mysite</h1></body></html>";
|
$body = "<html><head></head><body><h1>Mysite</h1></body></html>";
|
||||||
$response = new SS_HTTPResponse($body, 200);
|
$response = new SS_HTTPResponse($body, 200);
|
||||||
$this->assertEmpty($response->getHeader('Cache-Control'));
|
HTTPCacheControl::singleton()->publicCache();
|
||||||
|
|
||||||
HTTP::set_cache_age(30);
|
HTTP::set_cache_age(30);
|
||||||
|
|
||||||
HTTP::add_cache_headers($response);
|
HTTP::add_cache_headers($response);
|
||||||
$this->assertNotEmpty($response->getHeader('Cache-Control'));
|
$this->assertNotEmpty($response->getHeader('Cache-Control'));
|
||||||
|
|
||||||
// Ensure max-age is zero for development.
|
// Ensure cache headers are set correctly when disabled via config (e.g. when dev)
|
||||||
Config::inst()->update('Director', 'environment_type', 'dev');
|
Config::inst()->update('HTTP', 'disable_http_cache', true);
|
||||||
|
HTTPCacheControl::reset();
|
||||||
|
HTTPCacheControl::singleton()->publicCache();
|
||||||
|
HTTP::set_cache_age(30);
|
||||||
$response = new SS_HTTPResponse($body, 200);
|
$response = new SS_HTTPResponse($body, 200);
|
||||||
HTTP::add_cache_headers($response);
|
HTTP::add_cache_headers($response);
|
||||||
$this->assertContains('max-age=0', $response->getHeader('Cache-Control'));
|
$this->assertContains('no-cache', $response->getHeader('Cache-Control'));
|
||||||
|
$this->assertContains('no-store', $response->getHeader('Cache-Control'));
|
||||||
|
$this->assertContains('must-revalidate', $response->getHeader('Cache-Control'));
|
||||||
|
|
||||||
// Ensure max-age setting is respected in production.
|
// Ensure max-age setting is respected in production.
|
||||||
Config::inst()->update('Director', 'environment_type', 'live');
|
Config::inst()->remove('HTTP', 'disable_http_cache');
|
||||||
|
HTTPCacheControl::reset();
|
||||||
|
HTTPCacheControl::singleton()->publicCache();
|
||||||
|
HTTP::set_cache_age(30);
|
||||||
$response = new SS_HTTPResponse($body, 200);
|
$response = new SS_HTTPResponse($body, 200);
|
||||||
HTTP::add_cache_headers($response);
|
HTTP::add_cache_headers($response);
|
||||||
$this->assertContains('max-age=30', explode(', ', $response->getHeader('Cache-Control')));
|
$this->assertContains('max-age=30', $response->getHeader('Cache-Control'));
|
||||||
$this->assertNotContains('max-age=0', $response->getHeader('Cache-Control'));
|
$this->assertNotContains('max-age=0', $response->getHeader('Cache-Control'));
|
||||||
|
|
||||||
// Still "live": Ensure header's aren't overridden if already set (using purposefully different values).
|
// Still "live": Ensure header's aren't overridden if already set (using purposefully different values).
|
||||||
@ -36,33 +50,40 @@ class HTTPTest extends FunctionalTest {
|
|||||||
'Pragma' => 'no-cache',
|
'Pragma' => 'no-cache',
|
||||||
'Cache-Control' => 'max-age=0, no-cache, no-store',
|
'Cache-Control' => 'max-age=0, no-cache, no-store',
|
||||||
);
|
);
|
||||||
|
HTTPCacheControl::reset();
|
||||||
|
HTTPCacheControl::singleton()->publicCache();
|
||||||
|
HTTP::set_cache_age(30);
|
||||||
$response = new SS_HTTPResponse($body, 200);
|
$response = new SS_HTTPResponse($body, 200);
|
||||||
foreach($headers as $name => $value) {
|
foreach($headers as $name => $value) {
|
||||||
$response->addHeader($name, $value);
|
$response->addHeader($name, $value);
|
||||||
}
|
}
|
||||||
HTTP::add_cache_headers($response);
|
|
||||||
foreach($headers as $name => $value) {
|
|
||||||
$this->assertEquals($value, $response->getHeader($name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Expect a warning if the header is already set
|
||||||
|
$this->setExpectedException(
|
||||||
|
'PHPUnit_Framework_Error_Warning',
|
||||||
|
'Cache-Control header has already been set. '
|
||||||
|
. 'Please use HTTPCacheControl API to set caching options instead.'
|
||||||
|
);
|
||||||
|
HTTP::add_cache_headers($response);
|
||||||
|
}
|
||||||
|
|
||||||
public function testConfigVary() {
|
public function testConfigVary() {
|
||||||
$body = "<html><head></head><body><h1>Mysite</h1></body></html>";
|
$body = "<html><head></head><body><h1>Mysite</h1></body></html>";
|
||||||
$response = new SS_HTTPResponse($body, 200);
|
$response = new SS_HTTPResponse($body, 200);
|
||||||
Config::inst()->update('Director', 'environment_type', 'live');
|
|
||||||
HTTP::set_cache_age(30);
|
HTTP::set_cache_age(30);
|
||||||
HTTP::add_cache_headers($response);
|
HTTP::add_cache_headers($response);
|
||||||
|
|
||||||
$v = $response->getHeader('Vary');
|
$v = $response->getHeader('Vary');
|
||||||
$this->assertNotEmpty($v);
|
$this->assertNotEmpty($v);
|
||||||
|
|
||||||
$this->assertContains("Cookie", $v);
|
|
||||||
$this->assertContains("X-Forwarded-Protocol", $v);
|
$this->assertContains("X-Forwarded-Protocol", $v);
|
||||||
$this->assertContains("User-Agent", $v);
|
$this->assertContains("X-Requested-With", $v);
|
||||||
$this->assertContains("Accept", $v);
|
$this->assertNotContains("Cookie", $v);
|
||||||
|
$this->assertNotContains("User-Agent", $v);
|
||||||
|
$this->assertNotContains("Accept", $v);
|
||||||
|
|
||||||
Config::inst()->update('HTTP', 'vary', '');
|
Config::inst()->update('HTTP', 'vary', '');
|
||||||
|
HTTPCacheControl::reset();
|
||||||
|
|
||||||
$response = new SS_HTTPResponse($body, 200);
|
$response = new SS_HTTPResponse($body, 200);
|
||||||
HTTP::add_cache_headers($response);
|
HTTP::add_cache_headers($response);
|
||||||
|
Loading…
Reference in New Issue
Block a user