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:
cache_control:
max-age: 0
must-revalidate: "true"
no-transform: "true"
vary: "Cookie, X-Forwarded-Protocol, X-Forwarded-Proto, User-Agent, Accept"
no-cache: true
no-store: true
must-revalidate: true
vary: "X-Requested-With, X-Forwarded-Protocol"
SilverStripe\Core\Manifest\VersionProvider:
modules:
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;
use SilverStripe\Assets\File;
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Convert;
use InvalidArgumentException;
@ -37,6 +39,37 @@ class HTTP
*/
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.
*
@ -328,6 +361,7 @@ class HTTP
public static function set_cache_age($age)
{
self::$cache_age = $age;
HTTPCacheControlMiddleware::singleton()->setMaxAge(self::$cache_age);
}
/**
@ -376,148 +410,122 @@ class HTTP
*/
public static function add_cache_headers($body = null)
{
$cacheAge = self::$cache_age;
// Validate argument
if ($body && !($body instanceof HTTPResponse)) {
user_error("HTTP::add_cache_headers() must be passed an HTTPResponse object", E_USER_WARNING);
$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
// us trying.
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 HTTPCacheControlMiddleware API to set caching options instead.',
E_USER_WARNING
);
return;
}
$config = Config::forClass(__CLASS__);
// Get current cache control state
$cacheControl = HTTPCacheControlMiddleware::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
$responseHeaders = array();
$cacheControlHeaders = HTTP::config()->uninherited('cache_control');
// currently using a config setting to cancel this, seems to be so that the CMS caches ajax requests
if (function_exists('apache_request_headers') && static::config()->uninherited('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 no caching ajax requests, disable ajax if is ajax request
if (!$config->get('cache_ajax_requests') && Director::is_ajax()) {
$cacheControl->disableCache();
}
if ($cacheAge > 0) {
$cacheControlHeaders['max-age'] = self::$cache_age;
// Errors disable cache (unless some errors are cached intentionally by usercode)
if ($body && $body->isError()) {
// Even if publicCache(true) is specfied, errors will be uncachable
$cacheControl->disableCache(true);
}
// 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"] = "";
// To do: User-Agent should only be added in situations where you *are* actually
// varying according to user-agent.
$vary = HTTP::config()->uninherited('vary');
if ($vary && strlen($vary)) {
// split the current vary header into it's parts and merge it with the config settings
// 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;
}
} else {
// 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');
if (!$contentDisposition) {
$contentDisposition = $body->getHeader('Content-Disposition');
}
}
if ($body &&
if(
$body &&
Director::is_https() &&
isset($_SERVER['HTTP_USER_AGENT']) &&
strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE') == true &&
strstr($contentDisposition, 'attachment;')==true
strstr($contentDisposition, 'attachment;') == true &&
($cacheControl->hasDirective('no-cache') || $cacheControl->hasDirective('no-store'))
) {
// IE6-IE8 have problems saving files when https and no-cache are used
// 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.
$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";
}
$cacheControl->privateCache(true);
}
foreach ($cacheControlHeaders as $header => $value) {
if (is_null($value)) {
unset($cacheControlHeaders[$header]);
} elseif ((is_bool($value) && $value) || $value === "true") {
$cacheControlHeaders[$header] = $header;
} else {
$cacheControlHeaders[$header] = $header . "=" . $value;
}
}
$responseHeaders['Cache-Control'] = implode(', ', $cacheControlHeaders);
unset($cacheControlHeaders, $header, $value);
if (self::$modification_date && $cacheAge > 0) {
if (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)
// 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
$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));
$responseHeaders["ETag"] = $etag;
$etag = self::generateETag($body);
if ($etag) {
$responseHeaders['ETag'] = $etag;
// 304 response detection
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$ifModifiedSince = strtotime(stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']));
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 = !isset($_SERVER['HTTP_IF_NONE_MATCH']) || $_SERVER['HTTP_IF_NONE_MATCH'] == $etag;
$matchesEtag = $_SERVER['HTTP_IF_NONE_MATCH'] == $etag;
if ($ifModifiedSince >= self::$modification_date && $matchesEtag) {
if ($matchesEtag) {
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();
}
}
}
$expires = time() + $cacheAge;
$responseHeaders["Expires"] = self::gmt_date($expires);
}
}
if (self::$etag) {
$responseHeaders['ETag'] = self::$etag;
if ($cacheControl->hasDirective('max-age')) {
$expires = time() + $cacheControl->getDirective('max-age');
$responseHeaders["Expires"] = self::gmt_date($expires);
}
// etag needs to be a quoted string according to HTTP spec
@ -525,7 +533,10 @@ class HTTP
$responseHeaders['ETag'] = sprintf('"%s"', $responseHeaders['ETag']);
}
// Now that we've generated them, either output them or attach them to the HTTPResponse as appropriate
// 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
foreach($responseHeaders as $k => $v) {
if($body) {
// Set the header now if it's not already set.
@ -539,6 +550,35 @@ class HTTP
}
/**
* @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
* 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;
}
/**
* 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)
{
if (array_key_exists('flush', $request->getVars())) {
// Disable cache when flushing
HTTPCacheControlMiddleware::singleton()->disableCache(true);
foreach (ClassInfo::implementorsOf(Flushable::class) as $class) {
/** @var Flushable|string $class */
$class::flush();

View File

@ -41,6 +41,14 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
} catch (HTTPResponse_Exception $ex) {
$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);
return $response;
}
@ -262,6 +270,17 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
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.
* Returns false if there is no directive.
@ -280,6 +299,38 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
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.
* 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()
{
$cacheControl = array();
foreach ($this->state as $directive => $value) {
if (is_null($value)) {
$cacheControl = [];
foreach ($this->getDirectives() as $directive => $value) {
if ($value === true) {
$cacheControl[] = $directive;
} else {
$cacheControl[] = $directive . '=' . $value;

View File

@ -6,6 +6,7 @@ use BadMethodCallException;
use Exception;
use InvalidArgumentException;
use ReflectionClass;
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\Debug;
@ -657,7 +658,7 @@ class RequestHandler extends ViewableData
public function redirectBack()
{
// 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
// As a last resort redirect to base url