mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Refactor everything out of HTTP and into separate middlewares
This commit is contained in:
parent
3ea98cdb13
commit
687d0a6af1
@ -1,12 +1,6 @@
|
||||
---
|
||||
Name: coreconfig
|
||||
---
|
||||
SilverStripe\Control\HTTP:
|
||||
cache_control:
|
||||
no-cache: true
|
||||
no-store: true
|
||||
must-revalidate: true
|
||||
vary: "X-Requested-With, X-Forwarded-Protocol"
|
||||
SilverStripe\Core\Manifest\VersionProvider:
|
||||
modules:
|
||||
silverstripe/framework: Framework
|
||||
@ -15,5 +9,7 @@ Name: httpconfig-dev
|
||||
Only:
|
||||
environment: dev
|
||||
---
|
||||
SilverStripe\Control\HTTP:
|
||||
disable_http_cache: true
|
||||
# Set dev level to disabled with a higher forcing level
|
||||
SilverStripe\Control\Middleware\HTTPCacheControlMiddleware:
|
||||
defaultState: 'disabled'
|
||||
defaultForcingLevel: 3
|
||||
|
@ -11,6 +11,7 @@ SilverStripe\Core\Injector\Injector:
|
||||
SessionMiddleware: '%$SilverStripe\Control\Middleware\SessionMiddleware'
|
||||
RequestProcessorMiddleware: '%$SilverStripe\Control\RequestProcessor'
|
||||
FlushMiddleware: '%$SilverStripe\Control\Middleware\FlushMiddleware'
|
||||
ETagMiddleware: '%$SilverStripe\Control\Middleware\ETagMiddleware'
|
||||
HTTPCacheControleMiddleware: '%$SilverStripe\Control\Middleware\HTTPCacheControlMiddleware'
|
||||
CanonicalURLMiddleware: '%$SilverStripe\Control\Middleware\CanonicalURLMiddleware'
|
||||
SilverStripe\Control\Middleware\AllowedHostsMiddleware:
|
||||
|
@ -3,12 +3,14 @@
|
||||
namespace SilverStripe\Control;
|
||||
|
||||
use SilverStripe\Assets\File;
|
||||
use SilverStripe\Control\Middleware\ETagMiddleware;
|
||||
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\Core\Convert;
|
||||
use InvalidArgumentException;
|
||||
use finfo;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
|
||||
/**
|
||||
@ -19,30 +21,35 @@ class HTTP
|
||||
use Configurable;
|
||||
|
||||
/**
|
||||
* @deprecated 4.2..5.0 Use HTTPCacheControlMiddleware::singleton()->setMaxAge($age) instead
|
||||
* @var int
|
||||
*/
|
||||
protected static $cache_age = 0;
|
||||
|
||||
/**
|
||||
* @deprecated 4.2..5.0 Handled by HTTPCacheControlMiddleware
|
||||
* @var int
|
||||
*/
|
||||
protected static $modification_date = null;
|
||||
|
||||
/**
|
||||
* @deprecated 4.2..5.0 Handled by ETagMiddleware
|
||||
* @var string
|
||||
*/
|
||||
protected static $etag = null;
|
||||
|
||||
/**
|
||||
* @config
|
||||
*
|
||||
* @var bool
|
||||
* @deprecated 4.2..5.0 'HTTP.cache_ajax_requests config is deprecated.
|
||||
* Use HTTPCacheControlMiddleware::disableCache() instead'
|
||||
*/
|
||||
private static $cache_ajax_requests = true;
|
||||
private static $cache_ajax_requests = false;
|
||||
|
||||
/**
|
||||
* @config
|
||||
* @var bool
|
||||
* @deprecated 4.2..5.0 Use HTTPCacheControlMiddleware.defaultState/.defaultForcingLevel instead
|
||||
*/
|
||||
private static $disable_http_cache = false;
|
||||
|
||||
@ -66,6 +73,7 @@ class HTTP
|
||||
/**
|
||||
* Vary string; A comma separated list of var header names
|
||||
*
|
||||
* @deprecated 4.2..5.0 Handled by HTTPCacheMiddleware instead
|
||||
* @config
|
||||
* @var string|null
|
||||
*/
|
||||
@ -328,7 +336,6 @@ class HTTP
|
||||
* commonly known MIME types.
|
||||
*
|
||||
* @param string $filename
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_mime_type($filename)
|
||||
@ -369,32 +376,33 @@ class HTTP
|
||||
|
||||
/**
|
||||
* @param string $dateString
|
||||
* @deprecated 4.2..5.0 Use HTTPCacheControlMiddleware::registerModificationDate() instead
|
||||
*/
|
||||
public static function register_modification_date($dateString)
|
||||
{
|
||||
$timestamp = strtotime($dateString);
|
||||
if ($timestamp > self::$modification_date) {
|
||||
self::$modification_date = $timestamp;
|
||||
}
|
||||
Deprecation::notice('5.0', 'Use HTTPCacheControlMiddleware::registerModificationDate() instead');
|
||||
HTTPCacheControlMiddleware::singleton()->registerModificationDate($dateString);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $timestamp
|
||||
* @deprecated 4.2..5.0 Use HTTPCacheControlMiddleware::registerModificationDate() instead
|
||||
*/
|
||||
public static function register_modification_timestamp($timestamp)
|
||||
{
|
||||
if ($timestamp > self::$modification_date) {
|
||||
self::$modification_date = $timestamp;
|
||||
}
|
||||
Deprecation::notice('5.0', 'Use HTTPCacheControlMiddleware::registerModificationDate() instead');
|
||||
HTTPCacheControlMiddleware::singleton()->registerModificationDate($timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 4.2..5.0 Use ETagMiddleware instead
|
||||
* @param string $etag
|
||||
*/
|
||||
public static function register_etag($etag)
|
||||
{
|
||||
if (0 !== strpos($etag, '"')) {
|
||||
$etag = sprintf('"%s"', $etag);
|
||||
Deprecation::notice('5.0', 'Use ETagMiddleware instead');
|
||||
if (strpos($etag, '"') !== 0) {
|
||||
$etag = "\"{$etag}\"";
|
||||
}
|
||||
self::$etag = $etag;
|
||||
}
|
||||
@ -409,24 +417,21 @@ class HTTP
|
||||
* Omitting the $body argument or passing a string is deprecated; in these cases, the headers are
|
||||
* output directly.
|
||||
*
|
||||
* @param HTTPResponse $body
|
||||
* @param HTTPResponse $response
|
||||
* @deprecated 4.2..5.0 Headers are added automatically by HTTPCacheControlMiddleware instead.
|
||||
*/
|
||||
public static function add_cache_headers($body = null)
|
||||
public static function add_cache_headers($response = null)
|
||||
{
|
||||
// Validate argument
|
||||
if ($body && !($body instanceof HTTPResponse)) {
|
||||
user_error("HTTP::add_cache_headers() must be passed an HTTPResponse object", E_USER_WARNING);
|
||||
$body = null;
|
||||
}
|
||||
Deprecation::notice('5.0', 'Headers are added automatically by HTTPCacheControlMiddleware instead.');
|
||||
|
||||
// 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) {
|
||||
// Ensure a valid response object is provided
|
||||
if (!$response instanceof HTTPResponse) {
|
||||
user_error("HTTP::add_cache_headers() must be passed an HTTPResponse object", E_USER_WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
// Warn if already assigned cache-control headers
|
||||
if ($body && $body->getHeader('Cache-Control')) {
|
||||
if ($response->getHeader('Cache-Control')) {
|
||||
trigger_error(
|
||||
'Cache-Control header has already been set. '
|
||||
. 'Please use HTTPCacheControlMiddleware API to set caching options instead.',
|
||||
@ -435,157 +440,70 @@ class HTTP
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure a valid request object exists in the current context
|
||||
if (!Injector::inst()->has(HTTPRequest::class)) {
|
||||
user_error("HTTP::add_cache_headers() cannot work without a current HTTPRequest object", E_USER_WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var HTTPRequest $request */
|
||||
$request = Injector::inst()->get(HTTPRequest::class);
|
||||
|
||||
$config = Config::forClass(__CLASS__);
|
||||
|
||||
// Get current cache control state
|
||||
$cacheControl = HTTPCacheControlMiddleware::singleton();
|
||||
$cacheControlMiddleware = HTTPCacheControlMiddleware::singleton();
|
||||
$etagMiddleware = ETagMiddleware::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
|
||||
// templates and other data. will be overridden by forced publicCache(true) or privateCache(true) calls
|
||||
if ($config->get('disable_http_cache')) {
|
||||
$cacheControl->disableCache();
|
||||
Deprecation::notice('5.0', 'Use HTTPCacheControlMiddleware.defaultState/.defaultForcingLevel instead');
|
||||
$cacheControlMiddleware->disableCache();
|
||||
}
|
||||
|
||||
// Populate $responseHeaders with all the headers that we want to build
|
||||
$responseHeaders = [];
|
||||
|
||||
// if no caching ajax requests, disable ajax if is ajax request
|
||||
if (!$config->get('cache_ajax_requests') && Director::is_ajax()) {
|
||||
$cacheControl->disableCache();
|
||||
Deprecation::notice(
|
||||
'5.0',
|
||||
'HTTP.cache_ajax_requests config is deprecated. Use HTTPCacheControlMiddleware::disableCache() instead'
|
||||
);
|
||||
$cacheControlMiddleware->disableCache();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// split the current vary header into it's parts and merge it with the config settings
|
||||
// to create a list of unique vary values
|
||||
// Pass vary to middleware
|
||||
$configVary = $config->get('vary');
|
||||
$bodyVary = $body ? $body->getHeader('Vary') : '';
|
||||
$vary = self::combineVary($configVary, $bodyVary);
|
||||
if ($vary) {
|
||||
$responseHeaders['Vary'] = $vary;
|
||||
}
|
||||
|
||||
// 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 ($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 ($configVary) {
|
||||
Deprecation::notice('5.0', 'Use HTTPCacheControlMiddleware.defaultVary instead');
|
||||
$cacheControlMiddleware->addVary($configVary);
|
||||
}
|
||||
|
||||
// Set modification date
|
||||
if (self::$modification_date) {
|
||||
$responseHeaders["Last-Modified"] = self::gmt_date(self::$modification_date);
|
||||
Deprecation::notice('5.0', 'Use HTTPCacheControlMiddleware::registerModificationDate() instead');
|
||||
$cacheControlMiddleware->registerModificationDate(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
|
||||
$etag = self::generateETag($body);
|
||||
if ($etag) {
|
||||
$responseHeaders['ETag'] = $etag;
|
||||
|
||||
// 304 response detection
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ensure deprecated $etag property is assigned
|
||||
if (self::$etag && !$cacheControlMiddleware->hasDirective('no-store') && !$response->getHeader('ETag')) {
|
||||
Deprecation::notice('5.0', 'Etag should not be set explicitly');
|
||||
$response->addHeader('ETag', self::$etag);
|
||||
}
|
||||
|
||||
if ($cacheControl->hasDirective('max-age')) {
|
||||
$expires = time() + $cacheControl->getDirective('max-age');
|
||||
$responseHeaders["Expires"] = self::gmt_date($expires);
|
||||
// Run middleware
|
||||
$etagMiddleware->process($request, function (HTTPRequest $request) use ($cacheControlMiddleware, $response) {
|
||||
return $cacheControlMiddleware->process($request, function (HTTPRequest $request) use ($response) {
|
||||
return $response;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// etag needs to be a quoted string according to HTTP spec
|
||||
if (!empty($responseHeaders['ETag']) && 0 !== strpos($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
|
||||
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($body));
|
||||
}
|
||||
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)
|
||||
*
|
||||
* @param int $timestamp
|
||||
*
|
||||
* @deprecated 4.2..5.0 Inline if you need this
|
||||
* @return string
|
||||
*/
|
||||
public static function gmt_date($timestamp)
|
||||
@ -602,22 +520,4 @@ class HTTP
|
||||
{
|
||||
return self::$cache_age;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine vary strings
|
||||
*
|
||||
* @param string[] $varies Each vary as a separate arg
|
||||
* @return string
|
||||
*/
|
||||
protected static function combineVary(...$varies)
|
||||
{
|
||||
$cleanVaries = [];
|
||||
foreach ($varies as $vary) {
|
||||
$argVaries = array_filter(preg_split("/\s*,\s*/", trim($vary)));
|
||||
if ($argVaries) {
|
||||
$cleanVaries = array_merge($cleanVaries, $argVaries);
|
||||
}
|
||||
}
|
||||
return implode(', ', array_unique($cleanVaries));
|
||||
}
|
||||
}
|
||||
|
@ -217,7 +217,7 @@ class HTTPResponse
|
||||
* Return the HTTP header of the given name.
|
||||
*
|
||||
* @param string $header
|
||||
* @returns string
|
||||
* @return string
|
||||
*/
|
||||
public function getHeader($header)
|
||||
{
|
||||
|
102
src/Control/Middleware/ETagMiddleware.php
Normal file
102
src/Control/Middleware/ETagMiddleware.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Control\Middleware;
|
||||
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Core\Injector\Injectable;
|
||||
|
||||
/**
|
||||
* Generates and handle responses for etag header.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
class ETagMiddleware implements HTTPMiddleware
|
||||
{
|
||||
use Injectable;
|
||||
|
||||
/**
|
||||
* Generate response for the given request
|
||||
*
|
||||
* @param HTTPRequest $request
|
||||
* @param callable $delegate
|
||||
* @return HTTPResponse
|
||||
*/
|
||||
public function process(HTTPRequest $request, callable $delegate)
|
||||
{
|
||||
/** @var HTTPResponse $response */
|
||||
$response = $delegate($request);
|
||||
|
||||
// Ignore etag for no-store
|
||||
$cacheControl = $response->getHeader('Cache-Control');
|
||||
if ($cacheControl && strstr($cacheControl, 'no-store')) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Generate, assign, and conditionally check etag
|
||||
$etag = $this->generateETag($response);
|
||||
if ($etag) {
|
||||
$response->addHeader('ETag', $etag);
|
||||
|
||||
// Check if we have a match
|
||||
$ifNoneMatch = $request->getHeader('If-None-Match');
|
||||
if ($ifNoneMatch && $ifNoneMatch === $etag) {
|
||||
return $this->sendNotModified($request, $response);
|
||||
}
|
||||
}
|
||||
|
||||
// Check If-Modified-Since
|
||||
$ifModifiedSince = $request->getHeader('If-Modified-Since');
|
||||
$lastModified = $response->getHeader('Last-Modified');
|
||||
if ($ifModifiedSince && $lastModified && strtotime($ifModifiedSince) >= strtotime($lastModified)) {
|
||||
return $this->sendNotModified($request, $response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HTTPResponse|string $response
|
||||
* @return string|false
|
||||
*/
|
||||
protected function generateETag(HTTPResponse $response)
|
||||
{
|
||||
// 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($body));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sent not-modified response
|
||||
*
|
||||
* @param HTTPRequest $request
|
||||
* @param HTTPResponse $response
|
||||
* @return mixed
|
||||
*/
|
||||
protected function sendNotModified(HTTPRequest $request, HTTPResponse $response)
|
||||
{
|
||||
// 304 is invalid for destructive requests
|
||||
if (in_array($request->httpMethod(), ['POST', 'DELETE', 'PUT'])) {
|
||||
$response->setStatusCode(412);
|
||||
} else {
|
||||
$response->setStatusCode(304);
|
||||
}
|
||||
$response->setBody('');
|
||||
return $response;
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\Core\Injector\Injectable;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Core\Resettable;
|
||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
|
||||
class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
|
||||
{
|
||||
@ -43,14 +44,12 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
|
||||
$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();
|
||||
}
|
||||
// Update state based on current request and response objects
|
||||
$this->augmentState($request, $response);
|
||||
|
||||
// Add all headers to this response object
|
||||
$this->applyToResponse($response);
|
||||
|
||||
HTTP::add_cache_headers($response);
|
||||
if (isset($ex)) {
|
||||
throw $ex;
|
||||
}
|
||||
@ -87,12 +86,20 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Set default state
|
||||
*
|
||||
* @config
|
||||
* @var string
|
||||
*/
|
||||
protected static $defaultState = self::STATE_DISABLED;
|
||||
|
||||
/**
|
||||
* Current state
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $state = self::STATE_DISABLED;
|
||||
protected $state = null;
|
||||
|
||||
/**
|
||||
* Forcing level of previous setting; higher number wins
|
||||
@ -100,7 +107,39 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
|
||||
*w
|
||||
* @var int
|
||||
*/
|
||||
protected $forcingLevel = 0;
|
||||
protected $forcingLevel = null;
|
||||
|
||||
/**
|
||||
* List of vary keys
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $vary = null;
|
||||
|
||||
/**
|
||||
* Latest modification date for this response
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $modificationDate;
|
||||
|
||||
/**
|
||||
* Default vary
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $defaultVary = [
|
||||
"X-Requested-With" => true,
|
||||
"X-Forwarded-Protocol" => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Default forcing level
|
||||
*
|
||||
* @config
|
||||
* @var int
|
||||
*/
|
||||
private static $defaultForcingLevel = 0;
|
||||
|
||||
/**
|
||||
* Forcing level forced, optionally combined with one of the below.
|
||||
@ -148,6 +187,84 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
|
||||
'no-transform',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get current vary keys
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getVary()
|
||||
{
|
||||
// Explicitly set vary
|
||||
if (isset($this->vary)) {
|
||||
return $this->vary;
|
||||
}
|
||||
|
||||
// Load default from config
|
||||
$defaultVary = $this->config()->get('defaultVary');
|
||||
return array_keys(array_filter($defaultVary));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a vary
|
||||
*
|
||||
* @param string|array $vary
|
||||
* @return $this
|
||||
*/
|
||||
public function addVary($vary)
|
||||
{
|
||||
$combied = $this->combineVary($this->getVary(), $vary);
|
||||
$this->setVary($combied);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set vary
|
||||
*
|
||||
* @param array|string $vary
|
||||
* @return $this
|
||||
*/
|
||||
public function setVary($vary)
|
||||
{
|
||||
$this->vary = $this->combineVary($vary);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine vary strings/arrays into a single array, or normalise a single vary
|
||||
*
|
||||
* @param string|array[] $varies Each vary as a separate arg
|
||||
* @return array
|
||||
*/
|
||||
protected function combineVary(...$varies)
|
||||
{
|
||||
$merged = [];
|
||||
foreach ($varies as $vary) {
|
||||
if ($vary && is_string($vary)) {
|
||||
$vary = array_filter(preg_split("/\s*,\s*/", trim($vary)));
|
||||
}
|
||||
if ($vary && is_array($vary)) {
|
||||
$merged = array_merge($merged, $vary);
|
||||
}
|
||||
}
|
||||
return array_unique($merged);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register a modification date. Used to calculate the Modification-Date http header
|
||||
*
|
||||
* @param string|int $date Date string or timestamp
|
||||
* @return HTTPCacheControlMiddleware
|
||||
*/
|
||||
public function registerModificationDate($date)
|
||||
{
|
||||
$timestamp = is_numeric($date) ? $date : strtotime($date);
|
||||
if ($timestamp > $this->modificationDate) {
|
||||
$this->modificationDate = $timestamp;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current state. Should only be invoked internally after processing precedence rules.
|
||||
*
|
||||
@ -170,7 +287,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
|
||||
*/
|
||||
public function getState()
|
||||
{
|
||||
return $this->state;
|
||||
return $this->state ?: $this->config()->get('defaultState');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -190,7 +307,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
|
||||
protected function applyChangeLevel($level, $force)
|
||||
{
|
||||
$forcingLevel = $level + ($force ? self::LEVEL_FORCED : 0);
|
||||
if ($forcingLevel < $this->forcingLevel) {
|
||||
if ($forcingLevel < $this->getForcingLevel()) {
|
||||
return false;
|
||||
}
|
||||
$this->forcingLevel = $forcingLevel;
|
||||
@ -514,7 +631,7 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
|
||||
*/
|
||||
public function applyToResponse($response)
|
||||
{
|
||||
$headers = $this->generateHeaders();
|
||||
$headers = $this->generateHeadersFor($response);
|
||||
foreach ($headers as $name => $value) {
|
||||
$response->addHeader($name, $value);
|
||||
}
|
||||
@ -542,13 +659,17 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
|
||||
/**
|
||||
* Generate all headers to output
|
||||
*
|
||||
* @param HTTPResponse $response
|
||||
* @return array
|
||||
*/
|
||||
public function generateHeaders()
|
||||
public function generateHeadersFor(HTTPResponse $response)
|
||||
{
|
||||
return [
|
||||
return array_filter([
|
||||
'Last-Modified' => $this->generateLastModifiedHeader(),
|
||||
'Vary' => $this->generateVaryHeader($response),
|
||||
'Cache-Control' => $this->generateCacheHeader(),
|
||||
];
|
||||
'Expires' => $this->generateExpiresHeader(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -558,4 +679,98 @@ class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
|
||||
{
|
||||
Injector::inst()->unregisterNamedObject(__CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
protected function getForcingLevel()
|
||||
{
|
||||
if (isset($this->forcingLevel)) {
|
||||
return $this->forcingLevel;
|
||||
}
|
||||
return $this->config()->get('defaultForcingLevel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate vary http header
|
||||
*
|
||||
* @param HTTPResponse $response
|
||||
* @return string|null
|
||||
*/
|
||||
protected function generateVaryHeader(HTTPResponse $response)
|
||||
{
|
||||
// split the current vary header into it's parts and merge it with the config settings
|
||||
// to create a list of unique vary values
|
||||
$vary = $this->getVary();
|
||||
if ($response->getHeader('Vary')) {
|
||||
$vary = $this->combineVary($vary, $response->getHeader('Vary'));
|
||||
}
|
||||
if ($vary) {
|
||||
return implode(', ', $vary);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Last-Modified header
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function generateLastModifiedHeader()
|
||||
{
|
||||
if (!$this->modificationDate) {
|
||||
return null;
|
||||
}
|
||||
return gmdate('D, d M Y H:i:s', $this->modificationDate) . ' GMT';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Expires http header
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
protected function generateExpiresHeader()
|
||||
{
|
||||
$maxAge = $this->getDirective('max-age');
|
||||
if ($maxAge === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add now to max-age to generate expires
|
||||
$expires = DBDatetime::now()->getTimestamp() + $maxAge;
|
||||
return gmdate('D, d M Y H:i:s', $expires) . ' GMT';
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state based on current request and response objects
|
||||
*
|
||||
* @param HTTPRequest $request
|
||||
* @param HTTPResponse $response
|
||||
*/
|
||||
protected function augmentState(HTTPRequest $request, HTTPResponse $response)
|
||||
{
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 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.
|
||||
if ($request->getScheme() === 'https' &&
|
||||
preg_match('/.+MSIE (7|8).+/', $request->getHeader('User-Agent')) &&
|
||||
strstr($response->getHeader('Content-Disposition'), 'attachment;') == true &&
|
||||
($this->hasDirective('no-cache') || $this->hasDirective('no-store'))
|
||||
) {
|
||||
$this->privateCache(true);
|
||||
}
|
||||
|
||||
// Errors disable cache (unless some errors are cached intentionally by usercode)
|
||||
if ($response->isError()) {
|
||||
// Even if publicCache(true) is specfied, errors will be uncachable
|
||||
$this->disableCache(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use Exception;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use SilverStripe\Control\HTTP;
|
||||
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
@ -382,7 +383,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// Keep track of the modification date of all the data sourced to make this page
|
||||
// From this we create a Last-Modified HTTP header
|
||||
if (isset($record['LastEdited'])) {
|
||||
HTTP::register_modification_date($record['LastEdited']);
|
||||
HTTPCacheControlMiddleware::singleton()
|
||||
->registerModificationDate($record['LastEdited']);
|
||||
}
|
||||
|
||||
// Must be called after parent constructor
|
||||
|
@ -19,7 +19,9 @@ class HTTPCacheControlIntegrationTest extends FunctionalTest
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
Config::modify()->remove(HTTP::class, 'disable_http_cache');
|
||||
HTTPCacheControlMiddleware::config()
|
||||
->set('defaultState', 'disabled')
|
||||
->set('defaultForcingLevel', 0);
|
||||
HTTPCacheControlMiddleware::reset();
|
||||
}
|
||||
|
||||
|
@ -22,8 +22,10 @@ class HTTPTest extends FunctionalTest
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
// Remove dev-only config
|
||||
Config::modify()->remove(HTTP::class, 'disable_http_cache');
|
||||
// Set to disabled at null forcing level
|
||||
HTTPCacheControlMiddleware::config()
|
||||
->set('defaultState', 'disabled')
|
||||
->set('defaultForcingLevel', 0);
|
||||
HTTPCacheControlMiddleware::reset();
|
||||
}
|
||||
|
||||
@ -38,7 +40,9 @@ class HTTPTest extends FunctionalTest
|
||||
$this->assertNotEmpty($response->getHeader('Cache-Control'));
|
||||
|
||||
// Ensure cache headers are set correctly when disabled via config (e.g. when dev)
|
||||
Config::modify()->set(HTTP::class, 'disable_http_cache', true);
|
||||
HTTPCacheControlMiddleware::config()
|
||||
->set('defaultState', 'disabled')
|
||||
->set('defaultForcingLevel', HTTPCacheControlMiddleware::LEVEL_DISABLED);
|
||||
HTTPCacheControlMiddleware::reset();
|
||||
HTTPCacheControlMiddleware::singleton()->publicCache();
|
||||
HTTPCacheControlMiddleware::singleton()->setMaxAge(30);
|
||||
@ -49,7 +53,9 @@ class HTTPTest extends FunctionalTest
|
||||
$this->assertContains('must-revalidate', $response->getHeader('Cache-Control'));
|
||||
|
||||
// Ensure max-age setting is respected in production.
|
||||
Config::modify()->remove(HTTP::class, 'disable_http_cache');
|
||||
HTTPCacheControlMiddleware::config()
|
||||
->set('defaultState', 'disabled')
|
||||
->set('defaultForcingLevel', 0);
|
||||
HTTPCacheControlMiddleware::reset();
|
||||
HTTPCacheControlMiddleware::singleton()->publicCache();
|
||||
HTTPCacheControlMiddleware::singleton()->setMaxAge(30);
|
||||
|
@ -7,6 +7,16 @@ use SilverStripe\Dev\SapphireTest;
|
||||
|
||||
class HTTPCacheControlMiddlewareTest extends SapphireTest
|
||||
{
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
// Set to disabled at null forcing level
|
||||
HTTPCacheControlMiddleware::config()
|
||||
->set('defaultState', 'disabled')
|
||||
->set('defaultForcingLevel', 0);
|
||||
HTTPCacheControlMiddleware::reset();
|
||||
}
|
||||
|
||||
public function testCachingPriorities()
|
||||
{
|
||||
$hcc = new HTTPCacheControlMiddleware();
|
||||
|
Loading…
Reference in New Issue
Block a user