2018-06-12 07:17:17 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace SilverStripe\Control\Middleware;
|
|
|
|
|
|
|
|
use InvalidArgumentException;
|
2018-06-14 03:01:27 +02:00
|
|
|
use SilverStripe\Control\HTTP;
|
2018-06-12 07:17:17 +02:00
|
|
|
use SilverStripe\Control\HTTPRequest;
|
|
|
|
use SilverStripe\Control\HTTPResponse;
|
|
|
|
use SilverStripe\Control\HTTPResponse_Exception;
|
|
|
|
use SilverStripe\Core\Config\Configurable;
|
|
|
|
use SilverStripe\Core\Injector\Injectable;
|
|
|
|
use SilverStripe\Core\Injector\Injector;
|
|
|
|
use SilverStripe\Core\Resettable;
|
2018-06-13 07:56:47 +02:00
|
|
|
use SilverStripe\ORM\FieldType\DBDatetime;
|
2018-06-12 07:17:17 +02:00
|
|
|
|
|
|
|
class HTTPCacheControlMiddleware implements HTTPMiddleware, Resettable
|
|
|
|
{
|
|
|
|
use Configurable;
|
|
|
|
use Injectable;
|
|
|
|
|
|
|
|
const STATE_ENABLED = 'enabled';
|
|
|
|
|
|
|
|
const STATE_PUBLIC = 'public';
|
|
|
|
|
|
|
|
const STATE_PRIVATE = 'private';
|
|
|
|
|
|
|
|
const STATE_DISABLED = 'disabled';
|
|
|
|
|
2018-06-18 11:25:56 +02:00
|
|
|
const STATE_DEFAULT = 'default';
|
|
|
|
|
2018-06-12 07:17:17 +02:00
|
|
|
/**
|
|
|
|
* Generate response for the given request
|
|
|
|
*
|
|
|
|
* @todo Refactor HTTP::add_cache_headers() (e.g. etag handling) into this middleware
|
|
|
|
*
|
|
|
|
* @param HTTPRequest $request
|
|
|
|
* @param callable $delegate
|
|
|
|
* @return HTTPResponse
|
2018-06-12 13:53:07 +02:00
|
|
|
* @throws HTTPResponse_Exception
|
2018-06-12 07:17:17 +02:00
|
|
|
*/
|
|
|
|
public function process(HTTPRequest $request, callable $delegate)
|
|
|
|
{
|
|
|
|
try {
|
|
|
|
$response = $delegate($request);
|
|
|
|
} catch (HTTPResponse_Exception $ex) {
|
|
|
|
$response = $ex->getResponse();
|
|
|
|
}
|
2018-06-14 01:46:47 +02:00
|
|
|
if (!$response) {
|
|
|
|
return null;
|
|
|
|
}
|
2018-06-12 07:52:31 +02:00
|
|
|
|
2018-06-13 07:56:47 +02:00
|
|
|
// Update state based on current request and response objects
|
|
|
|
$this->augmentState($request, $response);
|
|
|
|
|
2018-06-14 03:01:27 +02:00
|
|
|
// Update state based on deprecated HTTP settings
|
|
|
|
HTTP::augmentState($request, $response);
|
|
|
|
|
2018-06-13 07:56:47 +02:00
|
|
|
// Add all headers to this response object
|
|
|
|
$this->applyToResponse($response);
|
2018-06-12 07:52:31 +02:00
|
|
|
|
2018-06-12 13:53:07 +02:00
|
|
|
if (isset($ex)) {
|
|
|
|
throw $ex;
|
|
|
|
}
|
2018-06-12 07:17:17 +02:00
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
|
2018-06-12 13:50:37 +02:00
|
|
|
/**
|
|
|
|
* List of states, each of which contains a key of standard directives.
|
2018-06-12 07:17:17 +02:00
|
|
|
* Each directive should either be a numeric value, true to enable,
|
|
|
|
* or (bool)false or null to disable.
|
|
|
|
* Top level key states include `disabled`, `private`, `public`, `enabled`
|
|
|
|
* in descending order of precedence.
|
|
|
|
*
|
|
|
|
* This allows directives to be set independently for individual states.
|
2018-06-12 13:50:37 +02:00
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
protected $stateDirectives = [
|
|
|
|
self::STATE_DISABLED => [
|
|
|
|
'no-cache' => true,
|
|
|
|
'no-store' => true,
|
|
|
|
'must-revalidate' => true,
|
2018-06-12 07:17:17 +02:00
|
|
|
],
|
|
|
|
self::STATE_PRIVATE => [
|
|
|
|
'private' => true,
|
|
|
|
'must-revalidate' => true,
|
|
|
|
],
|
|
|
|
self::STATE_PUBLIC => [
|
|
|
|
'public' => true,
|
|
|
|
'must-revalidate' => true,
|
|
|
|
],
|
|
|
|
self::STATE_ENABLED => [
|
|
|
|
'must-revalidate' => true,
|
|
|
|
],
|
2018-06-18 11:25:56 +02:00
|
|
|
self::STATE_DEFAULT => [
|
|
|
|
'no-cache' => true,
|
|
|
|
],
|
2018-06-12 07:17:17 +02:00
|
|
|
];
|
|
|
|
|
2018-06-13 07:56:47 +02:00
|
|
|
/**
|
|
|
|
* Set default state
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var string
|
|
|
|
*/
|
2018-06-18 11:25:56 +02:00
|
|
|
protected static $defaultState = self::STATE_DEFAULT;
|
2018-06-13 07:56:47 +02:00
|
|
|
|
2018-06-12 07:17:17 +02:00
|
|
|
/**
|
|
|
|
* Current state
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
2018-06-13 07:56:47 +02:00
|
|
|
protected $state = null;
|
2018-06-12 13:50:37 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Forcing level of previous setting; higher number wins
|
2018-06-14 03:31:34 +02:00
|
|
|
* Combination of consts below
|
|
|
|
*
|
2018-06-12 13:50:37 +02:00
|
|
|
* @var int
|
|
|
|
*/
|
2018-06-13 07:56:47 +02:00
|
|
|
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;
|
2018-06-12 13:50:37 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 = [
|
|
|
|
'public',
|
|
|
|
'private',
|
|
|
|
'no-cache',
|
|
|
|
'max-age',
|
|
|
|
's-maxage',
|
|
|
|
'must-revalidate',
|
|
|
|
'proxy-revalidate',
|
|
|
|
'no-store',
|
|
|
|
'no-transform',
|
|
|
|
];
|
2018-06-12 07:17:17 +02:00
|
|
|
|
2018-06-13 07:56:47 +02:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2018-06-14 03:31:34 +02:00
|
|
|
* Register a modification date. Used to calculate the "Last-Modified" HTTP header.
|
|
|
|
* Can be called multiple times, and will automatically retain the most recent date.
|
2018-06-13 07:56:47 +02:00
|
|
|
*
|
|
|
|
* @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;
|
|
|
|
}
|
|
|
|
|
2018-06-12 07:17:17 +02:00
|
|
|
/**
|
|
|
|
* Set current state. Should only be invoked internally after processing precedence rules.
|
|
|
|
*
|
|
|
|
* @param string $state
|
|
|
|
* @return $this
|
|
|
|
*/
|
2018-06-12 13:50:37 +02:00
|
|
|
protected function setState($state)
|
2018-06-12 07:17:17 +02:00
|
|
|
{
|
|
|
|
if (!array_key_exists($state, $this->stateDirectives)) {
|
|
|
|
throw new InvalidArgumentException("Invalid state {$state}");
|
|
|
|
}
|
|
|
|
$this->state = $state;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get current state
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getState()
|
|
|
|
{
|
2018-06-13 07:56:47 +02:00
|
|
|
return $this->state ?: $this->config()->get('defaultState');
|
2018-06-12 07:17:17 +02:00
|
|
|
}
|
|
|
|
|
2018-06-12 13:50:37 +02:00
|
|
|
/**
|
|
|
|
* 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);
|
2018-06-13 07:56:47 +02:00
|
|
|
if ($forcingLevel < $this->getForcingLevel()) {
|
2018-06-12 13:50:37 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
$this->forcingLevel = $forcingLevel;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Low level method for setting directives include any experimental or custom ones added via config.
|
2018-06-12 07:17:17 +02:00
|
|
|
* You need to specify the state (or states) to apply this directive to.
|
|
|
|
* Can also remove directives with false
|
2018-06-12 13:50:37 +02:00
|
|
|
*
|
2018-06-12 07:17:17 +02:00
|
|
|
* @param array|string $states State(s) to apply this directive to
|
2018-06-12 13:50:37 +02:00
|
|
|
* @param string $directive
|
|
|
|
* @param int|string|bool $value Flag to set for this value. Set to false to remove, or true to set.
|
2018-06-12 07:17:17 +02:00
|
|
|
* String or int value assign a specific value.
|
2018-06-12 13:50:37 +02:00
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setStateDirective($states, $directive, $value = true)
|
|
|
|
{
|
|
|
|
if ($value === null) {
|
|
|
|
throw new InvalidArgumentException("Invalid directive value");
|
2018-06-12 07:17:17 +02:00
|
|
|
}
|
2018-06-12 13:50:37 +02:00
|
|
|
// 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)) {
|
2018-06-12 07:17:17 +02:00
|
|
|
throw new InvalidArgumentException('Directive ' . $directive . ' is not allowed');
|
|
|
|
}
|
|
|
|
foreach ((array)$states as $state) {
|
2018-06-12 13:50:37 +02:00
|
|
|
if (!array_key_exists($state, $this->stateDirectives)) {
|
2018-06-12 07:17:17 +02:00
|
|
|
throw new InvalidArgumentException("Invalid state {$state}");
|
|
|
|
}
|
|
|
|
// Set or unset directive
|
|
|
|
if ($value === false) {
|
2018-06-12 13:50:37 +02:00
|
|
|
unset($this->stateDirectives[$state][$directive]);
|
2018-06-12 07:17:17 +02:00
|
|
|
} else {
|
|
|
|
$this->stateDirectives[$state][$directive] = $value;
|
|
|
|
}
|
|
|
|
}
|
2018-06-12 13:50:37 +02:00
|
|
|
return $this;
|
|
|
|
}
|
2018-06-12 07:17:17 +02:00
|
|
|
|
|
|
|
/**
|
2018-06-12 13:50:37 +02:00
|
|
|
* Low level method to set directives from an associative array
|
|
|
|
*
|
2018-06-12 07:17:17 +02:00
|
|
|
* @param array|string $states State(s) to apply this directive to
|
2018-06-12 13:50:37 +02:00
|
|
|
* @param array $directives
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setStateDirectivesFromArray($states, $directives)
|
2018-06-12 07:17:17 +02:00
|
|
|
{
|
|
|
|
foreach ($directives as $directive => $value) {
|
|
|
|
$this->setStateDirective($states, $directive, $value);
|
|
|
|
}
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2018-06-12 13:50:37 +02:00
|
|
|
/**
|
|
|
|
* Low level method for removing directives
|
|
|
|
*
|
2018-06-12 07:17:17 +02:00
|
|
|
* @param array|string $states State(s) to remove this directive from
|
2018-06-12 13:50:37 +02:00
|
|
|
* @param string $directive
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function removeStateDirective($states, $directive)
|
2018-06-12 07:17:17 +02:00
|
|
|
{
|
|
|
|
$this->setStateDirective($states, $directive, false);
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2018-06-12 13:50:37 +02:00
|
|
|
/**
|
|
|
|
* Low level method to check if a directive is currently set
|
|
|
|
*
|
2018-06-12 07:17:17 +02:00
|
|
|
* @param string $state State(s) to apply this directive to
|
2018-06-12 13:50:37 +02:00
|
|
|
* @param string $directive
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function hasStateDirective($state, $directive)
|
|
|
|
{
|
2018-06-12 07:17:17 +02:00
|
|
|
$directive = strtolower($directive);
|
|
|
|
return isset($this->stateDirectives[$state][$directive]);
|
2018-06-12 13:50:37 +02:00
|
|
|
}
|
2018-06-12 07:17:17 +02:00
|
|
|
|
2018-06-12 07:52:31 +02:00
|
|
|
/**
|
|
|
|
* Check if the current state has the given directive.
|
|
|
|
*
|
|
|
|
* @param string $directive
|
|
|
|
* @return bool
|
|
|
|
*/
|
2018-06-12 13:50:37 +02:00
|
|
|
public function hasDirective($directive)
|
2018-06-12 07:52:31 +02:00
|
|
|
{
|
|
|
|
return $this->hasStateDirective($this->getState(), $directive);
|
|
|
|
}
|
|
|
|
|
2018-06-12 13:50:37 +02:00
|
|
|
/**
|
|
|
|
* Low level method to get the value of a directive for a state.
|
2018-06-12 07:17:17 +02:00
|
|
|
* Returns false if there is no directive.
|
|
|
|
* True means the flag is set, otherwise the value of the directive.
|
2018-06-12 13:50:37 +02:00
|
|
|
*
|
2018-06-12 07:17:17 +02:00
|
|
|
* @param string $state
|
2018-06-12 13:50:37 +02:00
|
|
|
* @param string $directive
|
|
|
|
* @return int|string|bool
|
|
|
|
*/
|
|
|
|
public function getStateDirective($state, $directive)
|
|
|
|
{
|
|
|
|
$directive = strtolower($directive);
|
2018-06-12 07:17:17 +02:00
|
|
|
if (isset($this->stateDirectives[$state][$directive])) {
|
|
|
|
return $this->stateDirectives[$state][$directive];
|
|
|
|
}
|
|
|
|
return false;
|
2018-06-12 13:50:37 +02:00
|
|
|
}
|
2018-06-12 07:17:17 +02:00
|
|
|
|
2018-06-12 07:52:31 +02:00
|
|
|
/**
|
|
|
|
* Get the value of the given directive for the current state
|
|
|
|
*
|
|
|
|
* @param string $directive
|
|
|
|
* @return bool|int|string
|
|
|
|
*/
|
|
|
|
public function getDirective($directive)
|
|
|
|
{
|
2018-06-12 13:50:37 +02:00
|
|
|
return $this->getStateDirective($this->getState(), $directive);
|
2018-06-12 07:52:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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());
|
|
|
|
}
|
|
|
|
|
2018-06-12 13:50:37 +02:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
* 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)
|
|
|
|
{
|
|
|
|
// Affect all non-disabled states
|
2018-06-12 07:17:17 +02:00
|
|
|
$applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
|
|
|
|
if ($noStore) {
|
2018-06-12 13:50:37 +02:00
|
|
|
$this->setStateDirective($applyTo, 'no-store');
|
|
|
|
$this->removeStateDirective($applyTo, 'max-age');
|
|
|
|
$this->removeStateDirective($applyTo, 's-maxage');
|
|
|
|
} else {
|
|
|
|
$this->removeStateDirective($applyTo, 'no-store');
|
|
|
|
}
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Forces caches to submit the request to the origin server for validation before releasing a cached copy.
|
2018-06-12 07:17:17 +02:00
|
|
|
* Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
|
2018-06-12 13:50:37 +02:00
|
|
|
*
|
|
|
|
* @param bool $noCache
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setNoCache($noCache = true)
|
|
|
|
{
|
|
|
|
// Affect all non-disabled states
|
|
|
|
$applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
|
|
|
|
$this->setStateDirective($applyTo, 'no-cache', $noCache);
|
|
|
|
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.
|
2018-06-12 07:17:17 +02:00
|
|
|
* Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
|
2018-06-12 13:50:37 +02:00
|
|
|
*
|
|
|
|
* @param int $age
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setMaxAge($age)
|
|
|
|
{
|
|
|
|
// Affect all non-disabled states
|
|
|
|
$applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
|
|
|
|
$this->setStateDirective($applyTo, '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.
|
|
|
|
* Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
|
|
|
|
*
|
|
|
|
* @param int $age
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setSharedMaxAge($age)
|
|
|
|
{
|
|
|
|
// Affect all non-disabled states
|
|
|
|
$applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
|
|
|
|
$this->setStateDirective($applyTo, '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.
|
|
|
|
* Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
|
|
|
|
*
|
|
|
|
* @param bool $mustRevalidate
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setMustRevalidate($mustRevalidate = true)
|
|
|
|
{
|
|
|
|
$applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
|
|
|
|
$this->setStateDirective($applyTo, 'must-revalidate', $mustRevalidate);
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Simple way to set cache control header to a cacheable state.
|
2018-06-12 07:17:17 +02:00
|
|
|
*
|
|
|
|
* The resulting cache-control headers will be chosen from the 'enabled' set of directives.
|
2018-06-12 13:50:37 +02:00
|
|
|
*
|
|
|
|
* 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)) {
|
2018-06-12 07:17:17 +02:00
|
|
|
$this->setState(self::STATE_ENABLED);
|
|
|
|
}
|
|
|
|
return $this;
|
2018-06-12 13:50:37 +02:00
|
|
|
}
|
2018-06-12 07:17:17 +02:00
|
|
|
|
2018-06-12 13:50:37 +02:00
|
|
|
/**
|
2018-06-12 07:17:17 +02:00
|
|
|
* Simple way to set cache control header to a non-cacheable state.
|
2018-06-12 13:50:37 +02:00
|
|
|
* Use this method over `privateCache()` if you are unsure about caching details.
|
2018-06-12 07:17:17 +02:00
|
|
|
* Takes precendence over unforced `enableCache()`, `privateCache()` or `publicCache()` calls.
|
|
|
|
*
|
|
|
|
* The resulting cache-control headers will be chosen from the 'disabled' set of directives.
|
2018-06-12 13:50:37 +02:00
|
|
|
*
|
|
|
|
* 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)) {
|
|
|
|
$this->setState(self::STATE_DISABLED);
|
|
|
|
}
|
|
|
|
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.
|
2018-06-12 07:17:17 +02:00
|
|
|
*
|
|
|
|
* The resulting cache-control headers will be chosen from the 'private' set of directives.
|
2018-06-12 13:50:37 +02:00
|
|
|
*
|
|
|
|
* @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)) {
|
|
|
|
$this->setState(self::STATE_PRIVATE);
|
|
|
|
}
|
|
|
|
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)
|
2018-06-12 07:17:17 +02:00
|
|
|
*
|
|
|
|
* The resulting cache-control headers will be chosen from the 'private' set of directives.
|
2018-06-12 13:50:37 +02:00
|
|
|
*
|
|
|
|
* @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)
|
2018-06-12 07:17:17 +02:00
|
|
|
{
|
2018-06-12 13:50:37 +02:00
|
|
|
// Only execute this if its forcing level is high enough
|
2018-06-13 03:56:47 +02:00
|
|
|
if ($this->applyChangeLevel(self::LEVEL_PUBLIC, $force)) {
|
2018-06-12 13:50:37 +02:00
|
|
|
$this->setState(self::STATE_PUBLIC);
|
|
|
|
}
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-06-14 01:46:28 +02:00
|
|
|
* Generate all headers to add to this object
|
2018-06-12 13:50:37 +02:00
|
|
|
*
|
|
|
|
* @param HTTPResponse $response
|
|
|
|
*
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function applyToResponse($response)
|
|
|
|
{
|
2018-06-13 07:56:47 +02:00
|
|
|
$headers = $this->generateHeadersFor($response);
|
2018-06-12 13:50:37 +02:00
|
|
|
foreach ($headers as $name => $value) {
|
2018-06-14 01:46:28 +02:00
|
|
|
if (!$response->getHeader($name)) {
|
|
|
|
$response->addHeader($name, $value);
|
|
|
|
}
|
2018-06-12 13:50:37 +02:00
|
|
|
}
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate the cache header
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
protected function generateCacheHeader()
|
|
|
|
{
|
|
|
|
$cacheControl = [];
|
|
|
|
foreach ($this->getDirectives() as $directive => $value) {
|
|
|
|
if ($value === true) {
|
|
|
|
$cacheControl[] = $directive;
|
|
|
|
} else {
|
|
|
|
$cacheControl[] = $directive . '=' . $value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return implode(', ', $cacheControl);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate all headers to output
|
|
|
|
*
|
2018-06-13 07:56:47 +02:00
|
|
|
* @param HTTPResponse $response
|
2018-06-12 13:50:37 +02:00
|
|
|
* @return array
|
|
|
|
*/
|
2018-06-13 07:56:47 +02:00
|
|
|
public function generateHeadersFor(HTTPResponse $response)
|
2018-06-12 13:50:37 +02:00
|
|
|
{
|
2018-06-13 07:56:47 +02:00
|
|
|
return array_filter([
|
|
|
|
'Last-Modified' => $this->generateLastModifiedHeader(),
|
|
|
|
'Vary' => $this->generateVaryHeader($response),
|
2018-06-12 13:50:37 +02:00
|
|
|
'Cache-Control' => $this->generateCacheHeader(),
|
2018-06-13 07:56:47 +02:00
|
|
|
'Expires' => $this->generateExpiresHeader(),
|
|
|
|
]);
|
2018-06-12 13:50:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reset registered http cache control and force a fresh instance to be built
|
|
|
|
*/
|
|
|
|
public static function reset()
|
|
|
|
{
|
|
|
|
Injector::inst()->unregisterNamedObject(__CLASS__);
|
|
|
|
}
|
2018-06-13 07:56:47 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @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();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Errors disable cache (unless some errors are cached intentionally by usercode)
|
|
|
|
if ($response->isError()) {
|
2018-06-13 15:29:10 +02:00
|
|
|
// Even if publicCache(true) is specified, errors will be uncacheable
|
2018-06-13 07:56:47 +02:00
|
|
|
$this->disableCache(true);
|
|
|
|
}
|
2018-06-14 04:11:31 +02:00
|
|
|
|
|
|
|
// Don't cache redirects
|
|
|
|
if ($response->isRedirect()) {
|
|
|
|
$this->disableCache(true);
|
|
|
|
}
|
2018-06-13 07:56:47 +02:00
|
|
|
}
|
2018-06-12 07:17:17 +02:00
|
|
|
}
|