Manual merge up of 3.x changes to HTTP class

This commit is contained in:
Damian Mooyman 2018-06-12 17:52:31 +12:00
parent 76bf2ab21a
commit 442db3050c
5 changed files with 259 additions and 139 deletions

View File

@ -3,10 +3,17 @@ Name: coreconfig
--- ---
SilverStripe\Control\HTTP: SilverStripe\Control\HTTP:
cache_control: cache_control:
max-age: 0 no-cache: true
must-revalidate: "true" no-store: true
no-transform: "true" must-revalidate: true
vary: "Cookie, X-Forwarded-Protocol, X-Forwarded-Proto, User-Agent, Accept" vary: "X-Requested-With, X-Forwarded-Protocol"
SilverStripe\Core\Manifest\VersionProvider: SilverStripe\Core\Manifest\VersionProvider:
modules: modules:
silverstripe/framework: Framework silverstripe/framework: Framework
---
Name: httpconfig-dev
Only:
environment: dev
---
SilverStripe\Control\HTTP:
disable_http_cache: true

View File

@ -3,6 +3,8 @@
namespace SilverStripe\Control; namespace SilverStripe\Control;
use SilverStripe\Assets\File; use SilverStripe\Assets\File;
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use InvalidArgumentException; use InvalidArgumentException;
@ -37,6 +39,37 @@ 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 HTTPCacheControlMiddleware::__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 filename. * Turns a local system filename into a URL by comparing it to the script filename.
* *
@ -328,6 +361,7 @@ class HTTP
public static function set_cache_age($age) public static function set_cache_age($age)
{ {
self::$cache_age = $age; self::$cache_age = $age;
HTTPCacheControlMiddleware::singleton()->setMaxAge(self::$cache_age);
} }
/** /**
@ -376,169 +410,175 @@ class HTTP
*/ */
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 HTTPResponse)) { if ($body && !($body instanceof HTTPResponse)) {
user_error("HTTP::add_cache_headers() must be passed an HTTPResponse object", E_USER_WARNING); user_error("HTTP::add_cache_headers() must be passed an 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 HTTPResponse object to attach things to; no point in // The headers have been sent and we don't have an HTTPResponse object to attach things to; no point in
// us trying. // us trying.
if (headers_sent() && !$body) { if (headers_sent() && !$body) {
return; return;
} }
// Populate $responseHeaders with all the headers that we want to build // Warn if already assigned cache-control headers
$responseHeaders = array(); if ($body && $body->getHeader('Cache-Control')) {
trigger_error(
'Cache-Control header has already been set. '
. 'Please use HTTPCacheControlMiddleware API to set caching options instead.',
E_USER_WARNING
);
return;
}
$cacheControlHeaders = HTTP::config()->uninherited('cache_control'); $config = Config::forClass(__CLASS__);
// Get current cache control state
$cacheControl = HTTPCacheControlMiddleware::singleton();
// currently using a config setting to cancel this, seems to be so that the CMS caches ajax requests // if http caching is disabled by config, disable it - used on dev environments due to frequently changing
if (function_exists('apache_request_headers') && static::config()->uninherited('cache_ajax_requests')) { // templates and other data. will be overridden by forced publicCache() or privateCache() calls
$requestHeaders = array_change_key_case(apache_request_headers(), CASE_LOWER); if ($config->get('disable_http_cache')) {
if (isset($requestHeaders['x-requested-with']) $cacheControl->disableCache();
&& $requestHeaders['x-requested-with']=='XMLHttpRequest' }
) {
$cacheAge = 0;
}
}
if ($cacheAge > 0) { // Populate $responseHeaders with all the headers that we want to build
$cacheControlHeaders['max-age'] = self::$cache_age; $responseHeaders = array();
// Set empty pragma to avoid PHP's session_cache_limiter adding conflicting caching information, // if no caching ajax requests, disable ajax if is ajax request
// defaulting to "nocache" on most PHP configurations (see http://php.net/session_cache_limiter). if (!$config->get('cache_ajax_requests') && Director::is_ajax()) {
// Since it's a deprecated HTTP 1.0 option, all modern HTTP clients and proxies should $cacheControl->disableCache();
// 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 // Errors disable cache (unless some errors are cached intentionally by usercode)
// varying according to user-agent. if ($body && $body->isError()) {
$vary = HTTP::config()->uninherited('vary'); // Even if publicCache(true) is specfied, errors will be uncachable
if ($vary && strlen($vary)) { $cacheControl->disableCache(true);
$responseHeaders['Vary'] = $vary; }
}
} else {
$contentDisposition = null;
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 && // split the current vary header into it's parts and merge it with the config settings
Director::is_https() && // to create a list of unique vary values
isset($_SERVER['HTTP_USER_AGENT']) && $configVary = $config->get('vary');
strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')==true && $bodyVary = $body ? $body->getHeader('Vary') : '';
strstr($contentDisposition, 'attachment;')==true $vary = self::combineVary($configVary, $bodyVary);
) { if ($vary) {
// IE6-IE8 have problems saving files when https and no-cache are used $responseHeaders['Vary'] = $vary;
// (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, // deal with IE6-IE8 problems with https and no-cache
// defaulting to "nocache" on most PHP configurations (see http://php.net/session_cache_limiter). $contentDisposition = null;
// Since it's a deprecated HTTP 1.0 option, all modern HTTP clients and proxies should if($body) {
// prefer the caching information indicated through the "Cache-Control" header. // Grab header for checking. Unfortunately HTTPRequest uses a mistyped variant.
$responseHeaders["Pragma"] = ""; $contentDisposition = $body->getHeader('Content-Disposition');
} else { }
$cacheControlHeaders['no-cache'] = "true";
$cacheControlHeaders['no-store'] = "true";
}
}
foreach ($cacheControlHeaders as $header => $value) { if(
if (is_null($value)) { $body &&
unset($cacheControlHeaders[$header]); Director::is_https() &&
} elseif ((is_bool($value) && $value) || $value === "true") { isset($_SERVER['HTTP_USER_AGENT']) &&
$cacheControlHeaders[$header] = $header; strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE') == true &&
} else { strstr($contentDisposition, 'attachment;') == true &&
$cacheControlHeaders[$header] = $header . "=" . $value; ($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);
}
$responseHeaders['Cache-Control'] = implode(', ', $cacheControlHeaders); if (self::$modification_date) {
unset($cacheControlHeaders, $header, $value); $responseHeaders["Last-Modified"] = self::gmt_date(self::$modification_date);
}
if (self::$modification_date && $cacheAge > 0) { // if we can store the cache responses we should generate and send etags
$responseHeaders["Last-Modified"] = self::gmt_date(self::$modification_date); if (!$cacheControl->hasDirective('no-store')) {
// 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
// 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.
// 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
$etag = self::generateETag($body);
if ($etag) {
$responseHeaders['ETag'] = $etag;
// Chrome ignores Varies when redirecting back (http://code.google.com/p/chromium/issues/detail?id=79758) // 304 response detection
// which means that if you log out, you get redirected back to a page which Chrome then checks against if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
// last-modified (which passes, getting a 304) // As above, only 304 if the last request had all the same varies values
// when it shouldn't be trying to use that page at all because it's the "logged in" version. // (or the etag isn't passed as part of the request - but with chrome it always is)
// By also using and etag that includes both the modification date and all the varies $matchesEtag = $_SERVER['HTTP_IF_NONE_MATCH'] == $etag;
// values which we also check against we can catch this and not return a 304
$etagParts = array(self::$modification_date, serialize($_COOKIE));
$etagParts[] = Director::is_https() ? 'https' : 'http';
if (isset($_SERVER['HTTP_USER_AGENT'])) {
$etagParts[] = $_SERVER['HTTP_USER_AGENT'];
}
if (isset($_SERVER['HTTP_ACCEPT'])) {
$etagParts[] = $_SERVER['HTTP_ACCEPT'];
}
$etag = sha1(implode(':', $etagParts)); if ($matchesEtag) {
$responseHeaders["ETag"] = $etag; if ($body) {
$body->setStatusCode(304);
$body->setBody('');
} else {
// this is wrong, we need to send the same vary headers and so on
header('HTTP/1.0 304 Not Modified');
die();
}
}
}
}
}
// 304 response detection if ($cacheControl->hasDirective('max-age')) {
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { $expires = time() + $cacheControl->getDirective('max-age');
$ifModifiedSince = strtotime(stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE'])); $responseHeaders["Expires"] = self::gmt_date($expires);
}
// As above, only 304 if the last request had all the same varies values // etag needs to be a quoted string according to HTTP spec
// (or the etag isn't passed as part of the request - but with chrome it always is) if (!empty($responseHeaders['ETag']) && 0 !== strpos($responseHeaders['ETag'], '"')) {
$matchesEtag = !isset($_SERVER['HTTP_IF_NONE_MATCH']) || $_SERVER['HTTP_IF_NONE_MATCH'] == $etag; $responseHeaders['ETag'] = sprintf('"%s"', $responseHeaders['ETag']);
}
if ($ifModifiedSince >= self::$modification_date && $matchesEtag) { // Merge with cache control headers
if ($body) { $responseHeaders = array_merge($responseHeaders, $cacheControl->generateHeaders());
$body->setStatusCode(304);
$body->setBody('');
} else {
header('HTTP/1.0 304 Not Modified');
die();
}
}
}
$expires = time() + $cacheAge; // Now that we've generated them, either output them or attach them to the SS_HTTPResponse as appropriate
$responseHeaders["Expires"] = self::gmt_date($expires); foreach($responseHeaders as $k => $v) {
} if($body) {
// Set the header now if it's not already set.
if (self::$etag) { if ($body->getHeader($k) === null) {
$responseHeaders['ETag'] = self::$etag; $body->addHeader($k, $v);
} }
} elseif(!headers_sent()) {
// etag needs to be a quoted string according to HTTP spec header("$k: $v");
if (!empty($responseHeaders['ETag']) && 0 !== strpos($responseHeaders['ETag'], '"')) { }
$responseHeaders['ETag'] = sprintf('"%s"', $responseHeaders['ETag']); }
}
// Now that we've generated them, either output them or attach them to the HTTPResponse as appropriate
foreach ($responseHeaders as $k => $v) {
if ($body) {
// Set the header now if it's not already set.
if ($body->getHeader($k) === null) {
$body->addHeader($k, $v);
}
} elseif (!headers_sent()) {
header("$k: $v");
}
}
} }
/**
* @param HTTPResponse|string $response
*
* @return string|false
*/
protected static function generateETag($response)
{
// Explicit etag
if (self::$etag) {
return self::$etag;
}
// Existing e-tag
if ($response instanceof HTTPResponse && $response->getHeader('ETag')) {
return $response->getHeader('ETag');
}
// Generate etag from body
$body = $response instanceof HTTPResponse
? $response->getBody()
: $response;
if ($body) {
return sprintf('"%s"', md5($response));
}
return false;
}
/** /**
* Return an {@link http://www.faqs.org/rfcs/rfc2822 RFC 2822} date in the GMT timezone (a timestamp * 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 since January 1 1970 00:00:00 GMT) * is always in GMT: the number of seconds since January 1 1970 00:00:00 GMT)
@ -561,4 +601,22 @@ class HTTP
{ {
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));
}
} }

View File

@ -17,6 +17,9 @@ class FlushMiddleware implements HTTPMiddleware
public function process(HTTPRequest $request, callable $delegate) public function process(HTTPRequest $request, callable $delegate)
{ {
if (array_key_exists('flush', $request->getVars())) { if (array_key_exists('flush', $request->getVars())) {
// Disable cache when flushing
HTTPCacheControlMiddleware::singleton()->disableCache(true);
foreach (ClassInfo::implementorsOf(Flushable::class) as $class) { foreach (ClassInfo::implementorsOf(Flushable::class) as $class) {
/** @var Flushable|string $class */ /** @var Flushable|string $class */
$class::flush(); $class::flush();

View File

@ -41,6 +41,14 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
} catch (HTTPResponse_Exception $ex) { } catch (HTTPResponse_Exception $ex) {
$response = $ex->getResponse(); $response = $ex->getResponse();
} }
// If sessions exist we assume that the responses should not be cached by CDNs / proxies as we are
// likely to be supplying information relevant to the current user only
if ($request->getSession()->getAll()) {
// Don't force in case user code chooses to opt in to public caching
$this->privateCache();
}
HTTP::add_cache_headers($response); HTTP::add_cache_headers($response);
return $response; return $response;
} }
@ -262,6 +270,17 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
return isset($this->stateDirectives[$state][$directive]); return isset($this->stateDirectives[$state][$directive]);
} }
/**
* Check if the current state has the given directive.
*
* @param string $directive
* @return bool
*/
public function hasDirective($directive)
{
return $this->hasStateDirective($this->getState(), $directive);
}
/** /**
* Low level method to get the value of a directive for a state. * Low level method to get the value of a directive for a state.
* Returns false if there is no directive. * Returns false if there is no directive.
@ -280,6 +299,38 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
return false; return false;
} }
/**
* Get the value of the given directive for the current state
*
* @param string $directive
* @return bool|int|string
*/
public function getDirective($directive)
{
return $this->getStateDirective($this->getState(), $directive);
}
/**
* Get directives for the given state
*
* @param string $state
* @return array
*/
public function getStateDirectives($state)
{
return $this->stateDirectives[$state];
}
/**
* Get all directives for the currently active state
*
* @return array
*/
public function getDirectives()
{
return $this->getStateDirectives($this->getState());
}
/** /**
* The cache should not store anything about the client request or server response. * The cache should not store anything about the client request or server response.
* Affects all non-disabled states. Use setStateDirective() instead to set for a single state. * Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
@ -473,9 +524,9 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
*/ */
protected function generateCacheHeader() protected function generateCacheHeader()
{ {
$cacheControl = array(); $cacheControl = [];
foreach ($this->state as $directive => $value) { foreach ($this->getDirectives() as $directive => $value) {
if (is_null($value)) { if ($value === true) {
$cacheControl[] = $directive; $cacheControl[] = $directive;
} else { } else {
$cacheControl[] = $directive . '=' . $value; $cacheControl[] = $directive . '=' . $value;

View File

@ -6,6 +6,7 @@ use BadMethodCallException;
use Exception; use Exception;
use InvalidArgumentException; use InvalidArgumentException;
use ReflectionClass; use ReflectionClass;
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
@ -657,7 +658,7 @@ class RequestHandler extends ViewableData
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); HTTPCacheControlMiddleware::singleton()->disableCache(true);
// Prefer to redirect to ?BackURL, but fall back to Referer header // Prefer to redirect to ?BackURL, but fall back to Referer header
// As a last resort redirect to base url // As a last resort redirect to base url