mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge pull request #8166 from open-sausages/pulls/4/http-cache-middleware
NEW Add better HTTP cache-control manipulation (4.x branch)
This commit is contained in:
commit
bd84944c0d
@ -222,6 +222,7 @@ mappings:
|
||||
CookieJar: SilverStripe\Control\CookieJar
|
||||
Director: SilverStripe\Control\Director
|
||||
FlushRequestFilter: SilverStripe\Control\Middleware\FlushMiddleware
|
||||
HTTPCacheControl: SilverStripe\Control\Middleware\HTTPCacheControlMiddleware
|
||||
HTTP: SilverStripe\Control\HTTP
|
||||
SS_HTTPRequest: SilverStripe\Control\HTTPRequest
|
||||
SS_HTTPResponse: SilverStripe\Control\HTTPResponse
|
||||
|
@ -1,12 +1,15 @@
|
||||
---
|
||||
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"
|
||||
SilverStripe\Core\Manifest\VersionProvider:
|
||||
modules:
|
||||
silverstripe/framework: Framework
|
||||
---
|
||||
Name: httpconfig-dev
|
||||
Only:
|
||||
environment: dev
|
||||
---
|
||||
# Set dev level to disabled with a higher forcing level
|
||||
SilverStripe\Control\Middleware\HTTPCacheControlMiddleware:
|
||||
defaultState: 'disabled'
|
||||
defaultForcingLevel: 3
|
||||
|
@ -11,6 +11,8 @@ SilverStripe\Core\Injector\Injector:
|
||||
SessionMiddleware: '%$SilverStripe\Control\Middleware\SessionMiddleware'
|
||||
RequestProcessorMiddleware: '%$SilverStripe\Control\RequestProcessor'
|
||||
FlushMiddleware: '%$SilverStripe\Control\Middleware\FlushMiddleware'
|
||||
ChangeDetectionMiddleware: '%$SilverStripe\Control\Middleware\ChangeDetectionMiddleware'
|
||||
HTTPCacheControleMiddleware: '%$SilverStripe\Control\Middleware\HTTPCacheControlMiddleware'
|
||||
CanonicalURLMiddleware: '%$SilverStripe\Control\Middleware\CanonicalURLMiddleware'
|
||||
SilverStripe\Control\Middleware\AllowedHostsMiddleware:
|
||||
properties:
|
||||
@ -46,4 +48,3 @@ SilverStripe\Core\Injector\Injector:
|
||||
properties:
|
||||
ForceSSL: false
|
||||
ForceWWW: false
|
||||
|
||||
|
@ -76,6 +76,20 @@ functionality is available as an additional [Spam Protection](https://github.com
|
||||
module if required. The module provides an consistent API for allowing third-party spam protection handlers such as
|
||||
[Recaptcha](http://www.google.com/recaptcha/intro/) and [Mollom](https://mollom.com/) to work within the `Form` API.
|
||||
|
||||
## Data disclosure through HTTP Caching (since 4.2.0)
|
||||
|
||||
Forms, and particularly their responses, can contain sensitive or user-specific data.
|
||||
Forms can prepopulate submissions when a form is redisplayed with validation errors,
|
||||
and they by default contain CSRF tokens unique to the user's session.
|
||||
This data can inadvertently be stored either in a user's browser cache or in an intermediary
|
||||
cache such as a CDN or other caching-proxy. If incorrect `Cache-Control` headers are used, private data may be cached and
|
||||
accessible publicly through the CDN.
|
||||
|
||||
To ensure this doesn't happen SilverStripe adds `Cache-Control: no-store, no-cache, must-revalidate` headers to any
|
||||
forms that have validators or security tokens (all of them by default) applied to them; this ensures that CDNs
|
||||
(and browsers) will not cache these pages.
|
||||
See [/developer_guides/performance/http_cache_headers](Performance: HTTP Cache Headers).
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Security](../security)
|
||||
|
@ -1,7 +1,190 @@
|
||||
title: HTTP Cache Headers
|
||||
summary: Set the correct HTTP cache headers for your responses.
|
||||
|
||||
# Caching Headers
|
||||
# HTTP Cache Headers
|
||||
|
||||
## Overview
|
||||
|
||||
By default, SilverStripe sends headers which signal to HTTP caches
|
||||
that the response should be not considered cacheable.
|
||||
HTTP caches can either be intermediary caches (e.g. CDNs and proxies), or clients (e.g. browsers).
|
||||
The cache headers sent are `Cache-Control: no-store, no-cache, must-revalidate`;
|
||||
|
||||
HTTP caching can be a great way to speed up your website, but needs to be properly applied.
|
||||
Getting it wrong can accidentally expose draft pages or other protected content.
|
||||
The [Google Web Fundamentals](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private)
|
||||
are a great way to learn about HTTP caching.
|
||||
|
||||
## Cache Control Headers
|
||||
|
||||
### Overview
|
||||
|
||||
In order to support developers in making safe choices around HTTP caching,
|
||||
we're using a `HTTPCacheControlMiddleware` class to control if a response
|
||||
should be considered public or private. This is an abstraction on the
|
||||
`HTTPResponse->addHeader()` lowlevel API.
|
||||
|
||||
The `HTTPCacheControlMiddleware` API makes it easier to express your caching preferences
|
||||
without running the risk of overriding essential core safety measures.
|
||||
Most commonly, these APIs will prevent HTTP caching of draft content.
|
||||
|
||||
It will also prevent caching of content generated with an active session,
|
||||
since the system can't tell whether session data was used to vary the output.
|
||||
In this case, it's up to the developer to opt-in to caching,
|
||||
after ensuring that certain execution paths are safe despite of using sessions.
|
||||
|
||||
The system behaviour does not guard against accidentally caching "private" content,
|
||||
since there are too many variations under which output could be considered private
|
||||
(e.g. a custom "approval" flag on a comment object). It is up to
|
||||
the developer to ensure caching is used appropriately there.
|
||||
|
||||
The [api:SilverStripe\Control\Middleware\HTTPCacheControlMiddleware] class replaces
|
||||
(deprecated) caching methods in the `HTTP` helper class.
|
||||
It comes with methods which let developers safely interact with the `Cache-Control` header.
|
||||
|
||||
### disableCache()
|
||||
|
||||
Simple way to set cache control header to a non-cacheable state.
|
||||
Use this method over `privateCache()` if you are unsure about caching details.
|
||||
Takes precendence over unforced `enableCache()`, `privateCache()` or `publicCache()` calls.
|
||||
|
||||
Removes all state and replaces it with `no-cache, no-store, must-revalidate`. Although `no-store` is sufficient
|
||||
the others are added under [recommendation from Mozilla](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Examples)
|
||||
|
||||
Does not set `private` directive, use `privateCache()` if this is explicitly required
|
||||
([details](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private))
|
||||
|
||||
### enableCache()
|
||||
|
||||
Simple way to set cache control header to a cacheable state.
|
||||
Use this method over `publicCache()` if you are unsure about caching details.
|
||||
|
||||
Removes `no-store` and `no-cache` directives; other directives will remain in place.
|
||||
Use alongside `setMaxAge()` to indicate caching.
|
||||
|
||||
Does not set `public` directive. Usually, `setMaxAge()` is sufficient. Use `publicCache()` if this is explicitly required
|
||||
([details](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#public_vs_private))
|
||||
|
||||
### privateCache()
|
||||
|
||||
Advanced way to set cache control header to a non-cacheable state.
|
||||
Indicates that the response is intended for a single user and must not be stored by a shared cache.
|
||||
A private cache (e.g. Web Browser) may store the response. Also removes `public` as this is a contradictory directive.
|
||||
|
||||
### publicCache()
|
||||
|
||||
Advanced way to set cache control header to a cacheable state.
|
||||
Indicates that the response may be cached by any cache. (eg: CDNs, Proxies, Web browsers)
|
||||
Also removes `private` as this is a contradictory directive
|
||||
|
||||
### Priority
|
||||
|
||||
Each of these highlevel methods has a boolean `$force` parameter which determines
|
||||
their application priority regardless of execution order.
|
||||
The priority order is as followed, sorted in descending order
|
||||
(earlier items will overrule later items):
|
||||
|
||||
* `disableCache($force=true)`
|
||||
* `privateCache($force=true)`
|
||||
* `publicCache($force=true)`
|
||||
* `enableCache($force=true)`
|
||||
* `disableCache()`
|
||||
* `privateCache()`
|
||||
* `publicCache()`
|
||||
* `enableCache()`
|
||||
|
||||
## Cache Control Examples
|
||||
|
||||
### Global opt-in for page content
|
||||
|
||||
Enable caching for all page content (through `PageController`).
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
class PageController extends ContentController
|
||||
{
|
||||
public function init()
|
||||
{
|
||||
HTTPCacheControlMiddleware::singleton()
|
||||
->enableCache()
|
||||
->setMaxAge(60); // 1 minute
|
||||
|
||||
parent::init();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: SilverStripe will still override this preference when a session is active,
|
||||
a [CSRF token](/developer_guides/forms/form_security) token is present,
|
||||
or draft content has been requested.
|
||||
|
||||
### Opt-out for a particular controller action
|
||||
|
||||
If a controller output relies on session data, cookies,
|
||||
permission checks or other triggers for conditional output,
|
||||
you can disable caching either on a controller level
|
||||
(through `init()`) or for a particular action.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
class PageController extends ContentController
|
||||
{
|
||||
public function myprivateaction($request)
|
||||
{
|
||||
HTTPCacheControlMiddleware::singleton()
|
||||
->disableCache();
|
||||
|
||||
return $this->myPrivateResponse();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: SilverStripe will still override this preference when a session is active,
|
||||
a [CSRF token](/developer_guides/forms/form_security) token is present,
|
||||
or draft content has been requested.
|
||||
|
||||
### Global opt-in, ignoring session (advanced)
|
||||
|
||||
This can be helpful in situations where forms are embedded on the website.
|
||||
SilverStripe will still override this preference when draft content has been requested.
|
||||
CAUTION: This mode relies on a developer examining each execution path to ensure
|
||||
that no session data is used to vary output.
|
||||
|
||||
Use case: By default, forms include a [CSRF token](/developer_guides/forms/form_security)
|
||||
which starts a session with a value that's unique to the visitor, which makes the output uncacheable.
|
||||
But any subsequent requests by this visitor will also carry a session, leading to uncacheable output
|
||||
for this visitor. This is the case even if the output does not contain any forms,
|
||||
and does not vary for this particular visitor. Forms can also contain submission data
|
||||
when they're redisplayed after a validation error.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
class PageController extends ContentController
|
||||
{
|
||||
public function init()
|
||||
{
|
||||
HTTPCacheControlMiddleware::singleton()
|
||||
->enableCache($force=true) // DANGER ZONE
|
||||
->setMaxAge(60); // 1 minute
|
||||
|
||||
parent::init();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Defaults
|
||||
|
||||
By default, PHP adds caching headers that make the page appear purely dynamic. This isn't usually appropriate for most
|
||||
sites, even ones that are updated reasonably frequently. SilverStripe overrides the default settings with the following
|
||||
@ -14,39 +197,41 @@ headers:
|
||||
* Since a visitor cookie is set, the site won't be cached by proxies.
|
||||
* Ajax requests are never cached.
|
||||
|
||||
## Customizing Cache Headers
|
||||
## Max Age
|
||||
|
||||
### HTTP::set_cache_age
|
||||
The cache age determines the lifetime of your cache, in seconds.
|
||||
It only takes effect if you instruct the cache control
|
||||
that your response is cacheable in the first place (via `enableCache()` or via modifying the `HTTP.cache_control` defaults).
|
||||
|
||||
```php
|
||||
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
HTTPCacheControlMiddleware::singleton()
|
||||
->setMaxAge(60)
|
||||
```
|
||||
|
||||
Note that `setMaxAge(0)` is NOT sufficient to disable caching in all cases.
|
||||
|
||||
### Last Modified
|
||||
|
||||
Used to set the modification date to something more recent than the default. [api:DataObject::__construct] calls
|
||||
[api:HTTP::register_modification_date(] whenever a record comes from the database ensuring the newest date is present.
|
||||
|
||||
```php
|
||||
use SilverStripe\Control\HTTP;
|
||||
|
||||
HTTP::set_cache_age(0);
|
||||
```
|
||||
|
||||
Used to set the max-age component of the cache-control line, in seconds. Set it to 0 to disable caching; the "no-cache"
|
||||
clause in `Cache-Control` and `Pragma` will be included.
|
||||
|
||||
### HTTP::register_modification_date
|
||||
|
||||
|
||||
```php
|
||||
HTTP::register_modification_date('2014-10-10');
|
||||
```
|
||||
|
||||
Used to set the modification date to something more recent than the default. [DataObject::__construct](api:SilverStripe\ORM\DataObject::__construct) calls
|
||||
[HTTP::register_modification_date(](api:SilverStripe\Control\HTTP::register_modification_date() whenever a record comes from the database ensuring the newest date is present.
|
||||
### Vary
|
||||
|
||||
### Vary: cache header
|
||||
|
||||
By default, SilverStripe will output a `Vary` header (used by upstream caches for determining uniqueness)
|
||||
that looks like
|
||||
A `Vary` header tells caches which aspects of the response should be considered
|
||||
when calculating a cache key, usually in addition to the full URL.
|
||||
By default, SilverStripe will output a `Vary` header with the following content:
|
||||
|
||||
```
|
||||
Cookie, X-Forwarded-Protocol, User-Agent, Accept
|
||||
Vary: X-Requested-With, X-Forwarded-Protocol
|
||||
```
|
||||
|
||||
To change the value of the `Vary` header, you can change this value by specifying the header in configuration
|
||||
To change the value of the `Vary` header, you can change this value by specifying the header in configuration.
|
||||
|
||||
```yml
|
||||
SilverStripe\Control\HTTP:
|
||||
|
@ -770,6 +770,15 @@ class MySecureController extends Controller
|
||||
}
|
||||
```
|
||||
|
||||
## HTTP Caching Headers
|
||||
|
||||
Caching is hard. If you get it wrong, private or draft content might leak
|
||||
to unauthenticated users. We have created an abstraction which allows you to express
|
||||
your intent around HTTP caching without worrying too much about the details.
|
||||
See [/developer_guides/performances/http_cache_headers](Developer Guides > Performance > HTTP Cache Headers)
|
||||
for details on how to apply caching safely, and read Google's
|
||||
[Web Fundamentals on Caching](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching).
|
||||
|
||||
## Related
|
||||
|
||||
* [http://silverstripe.org/security-releases/](http://silverstripe.org/security-releases/)
|
||||
|
@ -201,3 +201,170 @@ SilverStripe\Core\Injector\Injector:
|
||||
args:
|
||||
disable-container: true
|
||||
```
|
||||
|
||||
### HTTP Cache Header changes
|
||||
|
||||
#### Overview
|
||||
|
||||
In order to support developers in making safe choices around HTTP caching,
|
||||
we're using a `HTTPCacheControlMiddleware` class to control if a response
|
||||
should be considered public or private. This is an abstraction on the
|
||||
`HTTPResponse->addHeader()` lowlevel API.
|
||||
|
||||
This change introduces smaller but necessary changes to HTTP caching headers
|
||||
sent by SilverStripe. If you are relying on HTTP caching in your implementation,
|
||||
or use modules such as [silverstripe/controllerpolicy](https://github.com/silverstripe/silverstripe-controllerpolicy),
|
||||
please review the implications of these changes below.
|
||||
|
||||
In short, these APIs make it easier to express your caching preferences
|
||||
without running the risk of overriding essential core safety measures.
|
||||
Most commonly, these APIs will prevent HTTP caching of draft content.
|
||||
|
||||
It will also prevent caching of content generated with an active session,
|
||||
since the system can't tell whether session data was used to vary the output.
|
||||
In this case, it's up to the developer to opt-in to caching,
|
||||
after ensuring that certain execution paths are safe despite of using sessions.
|
||||
|
||||
The system behaviour does not guard against accidentally caching "private" content,
|
||||
since there are too many variations under which output could be considered private
|
||||
(e.g. a custom "approval" flag on a comment object). It is up to
|
||||
the developer to ensure caching is used appropriately there.
|
||||
|
||||
By default, SilverStripe sends headers which signal to HTTP caches
|
||||
that the response should be considered not cacheable.
|
||||
|
||||
See [Developer Guide: Performance > HTTP Cache Headers](/developer_guide/performance/http_cache_headers)
|
||||
for details on the new API.
|
||||
|
||||
#### Disabling legacy cache headers
|
||||
|
||||
In order to forcibly disable all deprecated HTTP APIs you can set the below config:
|
||||
|
||||
```yaml
|
||||
SilverStripe\Control\HTTP:
|
||||
ignoreDeprecatedCaching: true
|
||||
```
|
||||
|
||||
This will ensure that any code paths that use the old API will not interefere with upgraded code
|
||||
that interferes with the new behaviour.
|
||||
|
||||
#### Example Usage
|
||||
|
||||
##### Global opt-in for page content
|
||||
|
||||
Enable caching for all page content (through `PageController`).
|
||||
|
||||
```diff
|
||||
<?php
|
||||
|
||||
-use SilverStripe\Control\HTTP;
|
||||
+use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
class PageController extends ContentController
|
||||
{
|
||||
public function init()
|
||||
{
|
||||
- HTTP::set_cache_age(60);
|
||||
+ HTTPCacheControlMiddleware::singleton()
|
||||
+ ->enableCache()
|
||||
+ ->setMaxAge(60); // 1 minute
|
||||
|
||||
parent::init();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: SilverStripe will still override this preference when a session is active,
|
||||
a [CSRF token](/developer_guides/forms/form_security) token is present,
|
||||
or draft content has been requested.
|
||||
|
||||
##### Opt-out for a particular controller action
|
||||
|
||||
If a controller output relies on session data, cookies,
|
||||
permission checks or other triggers for conditional output,
|
||||
you can disable caching either on a controller level
|
||||
(through `init()`) or for a particular action.
|
||||
|
||||
```diff
|
||||
<?php
|
||||
|
||||
-use SilverStripe\Control\HTTP;
|
||||
+use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
class PageController extends ContentController
|
||||
{
|
||||
public function myprivateaction($request)
|
||||
{
|
||||
- HTTP::set_cache_age(0);
|
||||
+ HTTPCacheControlMiddleware::singleton()
|
||||
+ ->disableCache();
|
||||
|
||||
return $this->myPrivateResponse();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: SilverStripe will still override this preference when a session is active,
|
||||
a [CSRF token](/developer_guides/forms/form_security) token is present,
|
||||
or draft content has been requested.
|
||||
|
||||
##### Global opt-in, ignoring session (advanced)
|
||||
|
||||
This can be helpful in situations where forms are embedded on the website.
|
||||
SilverStripe will still override this preference when draft content has been requested.
|
||||
CAUTION: This mode relies on a developer examining each execution path to ensure
|
||||
that no session data is used to vary output.
|
||||
|
||||
Use case: By default, forms include a [CSRF token](/developer_guides/forms/form_security)
|
||||
which starts a session with a value that's unique to the visitor, which makes the output uncacheable.
|
||||
But any subsequent requests by this visitor will also carry a session, leading to uncacheable output
|
||||
for this visitor. This is the case even if the output does not contain any forms,
|
||||
and does not vary for this particular visitor.
|
||||
|
||||
```diff
|
||||
<?php
|
||||
|
||||
-use SilverStripe\Control\HTTP;
|
||||
+use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\CMS\Controllers\ContentController;
|
||||
|
||||
class PageController extends ContentController
|
||||
{
|
||||
public function init()
|
||||
{
|
||||
- HTTP::set_cache_age(60);
|
||||
+ HTTPCacheControlMiddleware::singleton()
|
||||
+ ->enableCache($force=true) // DANGER ZONE
|
||||
+ ->setMaxAge(60); // 1 minute
|
||||
|
||||
parent::init();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Detailed Cache-Control Changes
|
||||
|
||||
* Added `Cache-Control: no-store` header to default responses,
|
||||
to prevent intermediary HTTP proxies (e.g. CDNs) from caching unless developers opt-in
|
||||
* Removed `Cache-Control: no-transform` header from default responses
|
||||
* Removed `Vary: Cookie` as an unreliable cache buster,
|
||||
rely on the existing `Cache-Control: no-store` defaults instead
|
||||
* Removed `Vary: Accept`, since it's very uncommon to vary content on
|
||||
the `Accept` headers submitted through the request,
|
||||
and it can significantly decrease the likelyhood of a cache hit.
|
||||
Note this is different from `Vary: Accept-Encoding`,
|
||||
which is important for compression (e.g. gzip), and usually added by
|
||||
other layers such as Apache's mod_gzip.
|
||||
* No longer sets `Last-Modified` date in HTTP response headers in `DataObject::__construct()`.
|
||||
Uses `ETag` calculation based on response body which is more accurate,
|
||||
and resilient against partial and object caching which can produce stale `Last-Modified` values.
|
||||
* Deprecated `HTTP::add_cache_headers()`. Headers are added automatically by `HTTPCacheControlMiddleware` instead.
|
||||
* Deprecated `HTTP::set_cache_age()`. Use `HTTPCacheControlMiddleware::singleton()->setMaxAge($age)`
|
||||
* Deprecated `HTTP.cache_ajax_requests`. Use `HTTPCacheControlMiddleware::disableCache()` instead
|
||||
* Deprecated `HTTP.modification_date`. Handled by `HTTPCacheControlMiddleware`
|
||||
* Deprecated `HTTP.disable_http_cache`. Use `HTTPCacheControlMiddleware.defaultState` and `defaultForcingLevel` instead
|
||||
* Deprecated `HTTP::register_modification_date()`. Use `HTTPCacheControlMiddleware::registerModificationDate()` instead
|
||||
* Deprecated `HTTP::register_modification_timestamp()`. Use `HTTPCacheControlMiddleware::registerModificationDate()` instead
|
||||
* Deprecated `HTTP::register_etag()`. Use `HTTPCacheControlMiddleware::ETagMiddleware()` instead
|
@ -258,9 +258,6 @@ class Controller extends RequestHandler implements TemplateGlobalProvider
|
||||
|
||||
//deal with content if appropriate
|
||||
ContentNegotiator::process($this->getResponse());
|
||||
|
||||
//add cache headers
|
||||
HTTP::add_cache_headers($this->getResponse());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -932,7 +932,6 @@ class Director implements TemplateGlobalProvider
|
||||
// Redirect to installer
|
||||
$response = new HTTPResponse();
|
||||
$response->redirect($destURL, 301);
|
||||
HTTP::add_cache_headers($response);
|
||||
throw new HTTPResponse_Exception($response);
|
||||
}
|
||||
|
||||
|
@ -3,10 +3,15 @@
|
||||
namespace SilverStripe\Control;
|
||||
|
||||
use SilverStripe\Assets\File;
|
||||
use SilverStripe\Control\Middleware\ChangeDetectionMiddleware;
|
||||
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;
|
||||
|
||||
/**
|
||||
* A class with HTTP-related helpers. Like Debug, this is more a bundle of methods than a class.
|
||||
@ -16,26 +21,70 @@ 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 ChangeDetectionMiddleware
|
||||
* @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 = false;
|
||||
|
||||
/**
|
||||
* @config
|
||||
* @var bool
|
||||
* @deprecated 4.2..5.0 Use HTTPCacheControlMiddleware.defaultState/.defaultForcingLevel instead
|
||||
*/
|
||||
private static $disable_http_cache = false;
|
||||
|
||||
/**
|
||||
* Set to true to disable all deprecated HTTP Cache settings
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $cache_ajax_requests = true;
|
||||
private static $ignoreDeprecatedCaching = false;
|
||||
|
||||
/**
|
||||
* Mapping of extension to mime types
|
||||
*
|
||||
* @var array
|
||||
* @config
|
||||
*/
|
||||
private static $MimeTypes = [];
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
/**
|
||||
* Vary string; A comma separated list of var header names
|
||||
*
|
||||
* @deprecated 4.2..5.0 Handled by HTTPCacheMiddleware instead
|
||||
* @config
|
||||
* @var string|null
|
||||
*/
|
||||
private static $vary = null;
|
||||
|
||||
/**
|
||||
* Turns a local system filename into a URL by comparing it to the script filename.
|
||||
@ -113,7 +162,7 @@ class HTTP
|
||||
}
|
||||
|
||||
// Replace attributes
|
||||
$attribs = array("src", "background", "a" => "href", "link" => "href", "base" => "href");
|
||||
$attribs = ["src", "background", "a" => "href", "link" => "href", "base" => "href"];
|
||||
$regExps = [];
|
||||
foreach ($attribs as $tag => $attrib) {
|
||||
if (!is_numeric($tag)) {
|
||||
@ -128,7 +177,7 @@ class HTTP
|
||||
}
|
||||
// Replace css styles
|
||||
// @todo - http://www.css3.info/preview/multiple-backgrounds/
|
||||
$styles = array('background-image', 'background', 'list-style-image', 'list-style', 'content');
|
||||
$styles = ['background-image', 'background', 'list-style-image', 'list-style', 'content'];
|
||||
foreach ($styles as $style) {
|
||||
$regExps[] = "/($style:[^;]*url *\\(\")([^\"]+)(\"\\))/i";
|
||||
$regExps[] = "/($style:[^;]*url *\\(')([^']+)('\\))/i";
|
||||
@ -189,7 +238,7 @@ class HTTP
|
||||
}
|
||||
|
||||
// Parse params and add new variable
|
||||
$params = array();
|
||||
$params = [];
|
||||
if (isset($parts['query'])) {
|
||||
parse_str($parts['query'], $params);
|
||||
}
|
||||
@ -197,7 +246,7 @@ class HTTP
|
||||
|
||||
// Generate URI segments and formatting
|
||||
$scheme = (isset($parts['scheme'])) ? $parts['scheme'] : 'http';
|
||||
$user = (isset($parts['user']) && $parts['user'] != '') ? $parts['user'] : '';
|
||||
$user = (isset($parts['user']) && $parts['user'] != '') ? $parts['user'] : '';
|
||||
|
||||
if ($user != '') {
|
||||
// format in either user:pass@host.com or user@host.com
|
||||
@ -209,13 +258,13 @@ class HTTP
|
||||
$path = (isset($parts['path']) && $parts['path'] != '') ? $parts['path'] : '';
|
||||
|
||||
// handle URL params which are existing / new
|
||||
$params = ($params) ? '?' . http_build_query($params, null, $separator) : '';
|
||||
$params = ($params) ? '?' . http_build_query($params, null, $separator) : '';
|
||||
|
||||
// keep fragments (anchors) intact.
|
||||
$fragment = (isset($parts['fragment']) && $parts['fragment'] != '') ? '#' . $parts['fragment'] : '';
|
||||
$fragment = (isset($parts['fragment']) && $parts['fragment'] != '') ? '#' . $parts['fragment'] : '';
|
||||
|
||||
// Recompile URI segments
|
||||
$newUri = $scheme . '://' . $user . $host . $port . $path . $params . $fragment;
|
||||
$newUri = $scheme . '://' . $user . $host . $port . $path . $params . $fragment;
|
||||
|
||||
if ($isRelative) {
|
||||
return Director::makeRelative($newUri);
|
||||
@ -248,14 +297,14 @@ class HTTP
|
||||
*/
|
||||
public static function findByTagAndAttribute($content, $attributes)
|
||||
{
|
||||
$regexes = array();
|
||||
$regexes = [];
|
||||
|
||||
foreach ($attributes as $tag => $attribute) {
|
||||
$regexes[] = "/<{$tag} [^>]*$attribute *= *([\"'])(.*?)\\1[^>]*>/i";
|
||||
$regexes[] = "/<{$tag} [^>]*$attribute *= *([^ \"'>]+)/i";
|
||||
}
|
||||
|
||||
$result = array();
|
||||
$result = [];
|
||||
|
||||
if ($regexes) {
|
||||
foreach ($regexes as $regex) {
|
||||
@ -275,7 +324,7 @@ class HTTP
|
||||
*/
|
||||
public static function getLinksIn($content)
|
||||
{
|
||||
return self::findByTagAndAttribute($content, array("a" => "href"));
|
||||
return self::findByTagAndAttribute($content, ["a" => "href"]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -285,7 +334,7 @@ class HTTP
|
||||
*/
|
||||
public static function getImagesIn($content)
|
||||
{
|
||||
return self::findByTagAndAttribute($content, array("img" => "src"));
|
||||
return self::findByTagAndAttribute($content, ["img" => "src"]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -294,7 +343,6 @@ class HTTP
|
||||
* commonly known MIME types.
|
||||
*
|
||||
* @param string $filename
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_mime_type($filename)
|
||||
@ -323,41 +371,45 @@ class HTTP
|
||||
/**
|
||||
* Set the maximum age of this page in web caches, in seconds.
|
||||
*
|
||||
* @deprecated 4.2..5.0 Use HTTPCacheControlMiddleware::singleton()->setMaxAge($age) instead
|
||||
* @param int $age
|
||||
*/
|
||||
public static function set_cache_age($age)
|
||||
{
|
||||
Deprecation::notice('5.0', 'Use HTTPCacheControlMiddleware::singleton()->setMaxAge($age) instead');
|
||||
self::$cache_age = $age;
|
||||
HTTPCacheControlMiddleware::singleton()->setMaxAge($age);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 ChangeDetectionMiddleware instead
|
||||
* @param string $etag
|
||||
*/
|
||||
public static function register_etag($etag)
|
||||
{
|
||||
if (0 !== strpos($etag, '"')) {
|
||||
$etag = sprintf('"%s"', $etag);
|
||||
Deprecation::notice('5.0', 'Use ChangeDetectionMiddleware instead');
|
||||
if (strpos($etag, '"') !== 0) {
|
||||
$etag = "\"{$etag}\"";
|
||||
}
|
||||
self::$etag = $etag;
|
||||
}
|
||||
@ -372,179 +424,110 @@ 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)
|
||||
{
|
||||
$cacheAge = self::$cache_age;
|
||||
Deprecation::notice('5.0', 'Headers are added automatically by HTTPCacheControlMiddleware instead.');
|
||||
|
||||
// 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) {
|
||||
// Skip if deprecated API is disabled
|
||||
if (Config::inst()->get(HTTP::class, 'ignoreDeprecatedCaching')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
if ($cacheAge > 0) {
|
||||
$cacheControlHeaders['max-age'] = self::$cache_age;
|
||||
|
||||
// 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)) {
|
||||
$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 &&
|
||||
Director::is_https() &&
|
||||
isset($_SERVER['HTTP_USER_AGENT']) &&
|
||||
strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')==true &&
|
||||
strstr($contentDisposition, 'attachment;')==true
|
||||
) {
|
||||
// IE6-IE8 have problems saving files when https and no-cache are used
|
||||
// (http://support.microsoft.com/kb/323308)
|
||||
// Note: this is also fixable by ticking "Do not save encrypted pages to disk" in advanced options.
|
||||
$cacheControlHeaders['max-age'] = 3;
|
||||
|
||||
// Set empty pragma to avoid PHP's session_cache_limiter adding conflicting caching information,
|
||||
// defaulting to "nocache" on most PHP configurations (see http://php.net/session_cache_limiter).
|
||||
// Since it's a deprecated HTTP 1.0 option, all modern HTTP clients and proxies should
|
||||
// prefer the caching information indicated through the "Cache-Control" header.
|
||||
$responseHeaders["Pragma"] = "";
|
||||
} else {
|
||||
$cacheControlHeaders['no-cache'] = "true";
|
||||
$cacheControlHeaders['no-store'] = "true";
|
||||
}
|
||||
// Warn if already assigned cache-control headers
|
||||
if ($response->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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
$responseHeaders['Cache-Control'] = implode(', ', $cacheControlHeaders);
|
||||
unset($cacheControlHeaders, $header, $value);
|
||||
/** @var HTTPRequest $request */
|
||||
$request = Injector::inst()->get(HTTPRequest::class);
|
||||
|
||||
if (self::$modification_date && $cacheAge > 0) {
|
||||
$responseHeaders["Last-Modified"] = self::gmt_date(self::$modification_date);
|
||||
|
||||
// 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;
|
||||
|
||||
// 304 response detection
|
||||
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
|
||||
$ifModifiedSince = strtotime(stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']));
|
||||
|
||||
// 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;
|
||||
|
||||
if ($ifModifiedSince >= self::$modification_date && $matchesEtag) {
|
||||
if ($body) {
|
||||
$body->setStatusCode(304);
|
||||
$body->setBody('');
|
||||
} else {
|
||||
header('HTTP/1.0 304 Not Modified');
|
||||
die();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$expires = time() + $cacheAge;
|
||||
$responseHeaders["Expires"] = self::gmt_date($expires);
|
||||
}
|
||||
|
||||
if (self::$etag) {
|
||||
$responseHeaders['ETag'] = self::$etag;
|
||||
}
|
||||
|
||||
// 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']);
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
// Run middleware
|
||||
ChangeDetectionMiddleware::singleton()->process($request, function (HTTPRequest $request) use ($response) {
|
||||
return HTTPCacheControlMiddleware::singleton()->process($request, function (HTTPRequest $request) use ($response) {
|
||||
return $response;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all deprecated HTTP cache settings are respected
|
||||
*
|
||||
* @deprecated 4.2..5.0 Use HTTPCacheControlMiddleware instead
|
||||
* @param HTTPRequest $request
|
||||
* @param HTTPResponse $response
|
||||
*/
|
||||
public static function augmentState(HTTPRequest $request, HTTPResponse $response)
|
||||
{
|
||||
// Skip if deprecated API is disabled
|
||||
$config = Config::forClass(HTTP::class);
|
||||
if ($config->get('ignoreDeprecatedCaching')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cacheControlMiddleware = 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(true) or privateCache(true) calls
|
||||
if ($config->get('disable_http_cache')) {
|
||||
Deprecation::notice('5.0', 'Use HTTPCacheControlMiddleware.defaultState/.defaultForcingLevel instead');
|
||||
$cacheControlMiddleware->disableCache();
|
||||
}
|
||||
|
||||
// if no caching ajax requests, disable ajax if is ajax request
|
||||
if (!$config->get('cache_ajax_requests') && Director::is_ajax()) {
|
||||
Deprecation::notice(
|
||||
'5.0',
|
||||
'HTTP.cache_ajax_requests config is deprecated. Use HTTPCacheControlMiddleware::disableCache() instead'
|
||||
);
|
||||
$cacheControlMiddleware->disableCache();
|
||||
}
|
||||
|
||||
// Pass vary to middleware
|
||||
$configVary = $config->get('vary');
|
||||
if ($configVary) {
|
||||
Deprecation::notice('5.0', 'Use HTTPCacheControlMiddleware.defaultVary instead');
|
||||
$cacheControlMiddleware->addVary($configVary);
|
||||
}
|
||||
|
||||
// Set modification date
|
||||
if (self::$modification_date) {
|
||||
Deprecation::notice('5.0', 'Use HTTPCacheControlMiddleware::registerModificationDate() instead');
|
||||
$cacheControlMiddleware->registerModificationDate(self::$modification_date);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
@ -19,7 +19,7 @@ class HTTPResponse
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static $status_codes = array(
|
||||
protected static $status_codes = [
|
||||
100 => 'Continue',
|
||||
101 => 'Switching Protocols',
|
||||
200 => 'OK',
|
||||
@ -61,20 +61,25 @@ class HTTPResponse
|
||||
503 => 'Service Unavailable',
|
||||
504 => 'Gateway Timeout',
|
||||
505 => 'HTTP Version Not Supported',
|
||||
);
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static $redirect_codes = array(
|
||||
protected static $redirect_codes = [
|
||||
301,
|
||||
302,
|
||||
303,
|
||||
304,
|
||||
305,
|
||||
307,
|
||||
308
|
||||
);
|
||||
308,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $protocolVersion = '1.0';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
@ -92,9 +97,9 @@ class HTTPResponse
|
||||
* @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
|
||||
* @var array
|
||||
*/
|
||||
protected $headers = array(
|
||||
protected $headers = [
|
||||
"content-type" => "text/html; charset=utf-8",
|
||||
);
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
@ -108,13 +113,35 @@ class HTTPResponse
|
||||
* @param int $statusCode The numeric status code - 200, 404, etc
|
||||
* @param string $statusDescription The text to be given alongside the status code.
|
||||
* See {@link setStatusCode()} for more information.
|
||||
* @param string $protocolVersion
|
||||
*/
|
||||
public function __construct($body = null, $statusCode = null, $statusDescription = null)
|
||||
public function __construct($body = null, $statusCode = null, $statusDescription = null, $protocolVersion = null)
|
||||
{
|
||||
$this->setBody($body);
|
||||
if ($statusCode) {
|
||||
$this->setStatusCode($statusCode, $statusDescription);
|
||||
}
|
||||
if (!$protocolVersion) {
|
||||
if (preg_match('/HTTP\/(?<version>\d+(\.\d+)?)/i', $_SERVER['SERVER_PROTOCOL'], $matches)) {
|
||||
$protocolVersion = $matches['version'];
|
||||
}
|
||||
}
|
||||
if ($protocolVersion) {
|
||||
$this->setProtocolVersion($protocolVersion);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The HTTP version used to respond to this request (typically 1.0 or 1.1)
|
||||
*
|
||||
* @param string $protocolVersion
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setProtocolVersion($protocolVersion)
|
||||
{
|
||||
$this->protocolVersion = $protocolVersion;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -123,6 +150,7 @@ class HTTPResponse
|
||||
* No newlines are allowed in the description.
|
||||
* If omitted, will default to the standard HTTP description
|
||||
* for the given $code value (see {@link $status_codes}).
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setStatusCode($code, $description = null)
|
||||
@ -146,6 +174,7 @@ class HTTPResponse
|
||||
* Caution: Will be overwritten by {@link setStatusCode()}.
|
||||
*
|
||||
* @param string $description
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setStatusDescription($description)
|
||||
@ -154,6 +183,14 @@ class HTTPResponse
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getProtocolVersion()
|
||||
{
|
||||
return $this->protocolVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
@ -167,7 +204,7 @@ class HTTPResponse
|
||||
*/
|
||||
public function getStatusDescription()
|
||||
{
|
||||
return str_replace(array("\r","\n"), '', $this->statusDescription);
|
||||
return str_replace(["\r", "\n"], '', $this->statusDescription);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,11 +220,12 @@ class HTTPResponse
|
||||
|
||||
/**
|
||||
* @param string $body
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setBody($body)
|
||||
{
|
||||
$this->body = $body ? (string) $body : $body; // Don't type-cast false-ish values, eg null is null not ''
|
||||
$this->body = $body ? (string)$body : $body; // Don't type-cast false-ish values, eg null is null not ''
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -204,6 +242,7 @@ class HTTPResponse
|
||||
*
|
||||
* @param string $header Example: "content-type"
|
||||
* @param string $value Example: "text/xml"
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addHeader($header, $value)
|
||||
@ -217,7 +256,8 @@ class HTTPResponse
|
||||
* Return the HTTP header of the given name.
|
||||
*
|
||||
* @param string $header
|
||||
* @returns string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHeader($header)
|
||||
{
|
||||
@ -241,6 +281,7 @@ class HTTPResponse
|
||||
* e.g. "Content-Type".
|
||||
*
|
||||
* @param string $header
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function removeHeader($header)
|
||||
@ -253,6 +294,7 @@ class HTTPResponse
|
||||
/**
|
||||
* @param string $dest
|
||||
* @param int $code
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function redirect($dest, $code = 302)
|
||||
@ -322,7 +364,7 @@ EOT
|
||||
);
|
||||
header($method);
|
||||
foreach ($this->getHeaders() as $header => $value) {
|
||||
header("{$header}: {$value}", true, $this->getStatusCode());
|
||||
header("{$header}: {$value}", true, $this->getStatusCode());
|
||||
}
|
||||
} elseif ($this->getStatusCode() >= 300) {
|
||||
// It's critical that these status codes are sent; we need to report a failure if not.
|
||||
@ -351,9 +393,9 @@ EOT
|
||||
/** @var HandlerInterface $handler */
|
||||
$handler = Injector::inst()->get(HandlerInterface::class);
|
||||
$formatter = $handler->getFormatter();
|
||||
echo $formatter->format(array(
|
||||
'code' => $this->statusCode
|
||||
));
|
||||
echo $formatter->format([
|
||||
'code' => $this->statusCode,
|
||||
]);
|
||||
} else {
|
||||
echo $this->body;
|
||||
}
|
||||
@ -379,4 +421,23 @@ EOT
|
||||
{
|
||||
return in_array($this->getStatusCode(), self::$redirect_codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* The HTTP response represented as a raw string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$headers = [];
|
||||
foreach ($this->getHeaders() as $header => $values) {
|
||||
foreach ((array)$values as $value) {
|
||||
$headers[] = sprintf('%s: %s', $header, $value);
|
||||
}
|
||||
}
|
||||
return
|
||||
sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->getStatusCode(), $this->getStatusDescription()) . "\r\n" .
|
||||
implode("\r\n", $headers) . "\r\n" . "\r\n" .
|
||||
$this->getBody();
|
||||
}
|
||||
}
|
||||
|
@ -407,7 +407,6 @@ class CanonicalURLMiddleware implements HTTPMiddleware
|
||||
// Force redirect
|
||||
$response = HTTPResponse::create();
|
||||
$response->redirect($url, $this->getRedirectType());
|
||||
HTTP::add_cache_headers($response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
94
src/Control/Middleware/ChangeDetectionMiddleware.php
Normal file
94
src/Control/Middleware/ChangeDetectionMiddleware.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Control\Middleware;
|
||||
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Core\Injector\Injectable;
|
||||
|
||||
/**
|
||||
* Handles internal change detection via etag / ifmodifiedsince headers,
|
||||
* conditonally sending a 304 not modified if possible.
|
||||
*/
|
||||
class ChangeDetectionMiddleware 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);
|
||||
if (!$response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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 === $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
|
||||
$etag = $response->getHeader('ETag');
|
||||
if ($etag) {
|
||||
return $etag;
|
||||
}
|
||||
|
||||
// Generate etag from body
|
||||
return sprintf('"%s"', md5($response->getBody()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
779
src/Control/Middleware/HTTPCacheControlMiddleware.php
Normal file
779
src/Control/Middleware/HTTPCacheControlMiddleware.php
Normal file
@ -0,0 +1,779 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Control\Middleware;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\Control\HTTP;
|
||||
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;
|
||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @throws HTTPResponse_Exception
|
||||
*/
|
||||
public function process(HTTPRequest $request, callable $delegate)
|
||||
{
|
||||
try {
|
||||
$response = $delegate($request);
|
||||
} catch (HTTPResponse_Exception $ex) {
|
||||
$response = $ex->getResponse();
|
||||
}
|
||||
if (!$response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update state based on current request and response objects
|
||||
$this->augmentState($request, $response);
|
||||
|
||||
// Update state based on deprecated HTTP settings
|
||||
HTTP::augmentState($request, $response);
|
||||
|
||||
// Add all headers to this response object
|
||||
$this->applyToResponse($response);
|
||||
|
||||
if (isset($ex)) {
|
||||
throw $ex;
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of states, each of which contains a key of standard directives.
|
||||
* 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.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $stateDirectives = [
|
||||
self::STATE_DISABLED => [
|
||||
'no-cache' => true,
|
||||
'no-store' => true,
|
||||
'must-revalidate' => true,
|
||||
],
|
||||
self::STATE_PRIVATE => [
|
||||
'private' => true,
|
||||
'must-revalidate' => true,
|
||||
],
|
||||
self::STATE_PUBLIC => [
|
||||
'public' => true,
|
||||
'must-revalidate' => true,
|
||||
],
|
||||
self::STATE_ENABLED => [
|
||||
'must-revalidate' => true,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Set default state
|
||||
*
|
||||
* @config
|
||||
* @var string
|
||||
*/
|
||||
protected static $defaultState = self::STATE_DISABLED;
|
||||
|
||||
/**
|
||||
* Current state
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $state = null;
|
||||
|
||||
/**
|
||||
* Forcing level of previous setting; higher number wins
|
||||
* Combination of consts below
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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',
|
||||
];
|
||||
|
||||
/**
|
||||
* 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 "Last-Modified" HTTP header.
|
||||
* Can be called multiple times, and will automatically retain the most recent date.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @param string $state
|
||||
* @return $this
|
||||
*/
|
||||
protected function setState($state)
|
||||
{
|
||||
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()
|
||||
{
|
||||
return $this->state ?: $this->config()->get('defaultState');
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the cache to apply a change with a given level, optionally
|
||||
* modifying it with a force flag to increase priority of this action.
|
||||
*
|
||||
* If the apply level was successful, the change is made and the internal level
|
||||
* threshold is incremented.
|
||||
*
|
||||
* @param int $level Priority of the given change
|
||||
* @param bool $force If usercode has requested this action is forced to a higher priority.
|
||||
* Note: Even if $force is set to true, other higher-priority forced changes can still
|
||||
* cause a change to be rejected if it is below the required threshold.
|
||||
* @return bool True if the given change is accepted, and that the internal
|
||||
* level threshold is updated (if necessary) to the new minimum level.
|
||||
*/
|
||||
protected function applyChangeLevel($level, $force)
|
||||
{
|
||||
$forcingLevel = $level + ($force ? self::LEVEL_FORCED : 0);
|
||||
if ($forcingLevel < $this->getForcingLevel()) {
|
||||
return false;
|
||||
}
|
||||
$this->forcingLevel = $forcingLevel;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Low level method for setting directives include any experimental or custom ones added via config.
|
||||
* You need to specify the state (or states) to apply this directive to.
|
||||
* Can also remove directives with false
|
||||
*
|
||||
* @param array|string $states State(s) to apply this directive to
|
||||
* @param string $directive
|
||||
* @param int|string|bool $value Flag to set for this value. Set to false to remove, or true to set.
|
||||
* String or int value assign a specific value.
|
||||
* @return $this
|
||||
*/
|
||||
public function setStateDirective($states, $directive, $value = true)
|
||||
{
|
||||
if ($value === null) {
|
||||
throw new InvalidArgumentException("Invalid directive value");
|
||||
}
|
||||
// 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)) {
|
||||
throw new InvalidArgumentException('Directive ' . $directive . ' is not allowed');
|
||||
}
|
||||
foreach ((array)$states as $state) {
|
||||
if (!array_key_exists($state, $this->stateDirectives)) {
|
||||
throw new InvalidArgumentException("Invalid state {$state}");
|
||||
}
|
||||
// Set or unset directive
|
||||
if ($value === false) {
|
||||
unset($this->stateDirectives[$state][$directive]);
|
||||
} else {
|
||||
$this->stateDirectives[$state][$directive] = $value;
|
||||
}
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Low level method to set directives from an associative array
|
||||
*
|
||||
* @param array|string $states State(s) to apply this directive to
|
||||
* @param array $directives
|
||||
* @return $this
|
||||
*/
|
||||
public function setStateDirectivesFromArray($states, $directives)
|
||||
{
|
||||
foreach ($directives as $directive => $value) {
|
||||
$this->setStateDirective($states, $directive, $value);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Low level method for removing directives
|
||||
*
|
||||
* @param array|string $states State(s) to remove this directive from
|
||||
* @param string $directive
|
||||
* @return $this
|
||||
*/
|
||||
public function removeStateDirective($states, $directive)
|
||||
{
|
||||
$this->setStateDirective($states, $directive, false);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Low level method to check if a directive is currently set
|
||||
*
|
||||
* @param string $state State(s) to apply this directive to
|
||||
* @param string $directive
|
||||
* @return bool
|
||||
*/
|
||||
public function hasStateDirective($state, $directive)
|
||||
{
|
||||
$directive = strtolower($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.
|
||||
* Returns false if there is no directive.
|
||||
* True means the flag is set, otherwise the value of the directive.
|
||||
*
|
||||
* @param string $state
|
||||
* @param string $directive
|
||||
* @return int|string|bool
|
||||
*/
|
||||
public function getStateDirective($state, $directive)
|
||||
{
|
||||
$directive = strtolower($directive);
|
||||
if (isset($this->stateDirectives[$state][$directive])) {
|
||||
return $this->stateDirectives[$state][$directive];
|
||||
}
|
||||
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.
|
||||
* 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
|
||||
$applyTo = [self::STATE_ENABLED, self::STATE_PRIVATE, self::STATE_PUBLIC];
|
||||
if ($noStore) {
|
||||
$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.
|
||||
* Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
|
||||
*
|
||||
* @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.
|
||||
* Affects all non-disabled states. Use setStateDirective() instead to set for a single state.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* The resulting cache-control headers will be chosen from the 'enabled' set of directives.
|
||||
*
|
||||
* 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)) {
|
||||
$this->setState(self::STATE_ENABLED);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple way to set cache control header to a non-cacheable state.
|
||||
* Use this method over `privateCache()` if you are unsure about caching details.
|
||||
* Takes precendence over unforced `enableCache()`, `privateCache()` or `publicCache()` calls.
|
||||
*
|
||||
* The resulting cache-control headers will be chosen from the 'disabled' set of directives.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The resulting cache-control headers will be chosen from the 'private' set of directives.
|
||||
*
|
||||
* @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)
|
||||
*
|
||||
* The resulting cache-control headers will be chosen from the 'private' set of directives.
|
||||
*
|
||||
* @see https://docs.silverstripe.org/en/developer_guides/performance/http_cache_headers/
|
||||
* @param bool $force Force the cache to public even if it's private, unless it's been forced private
|
||||
* @return $this
|
||||
*/
|
||||
public function publicCache($force = false)
|
||||
{
|
||||
// Only execute this if its forcing level is high enough
|
||||
if ($this->applyChangeLevel(self::LEVEL_PUBLIC, $force)) {
|
||||
$this->setState(self::STATE_PUBLIC);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all headers to add to this object
|
||||
*
|
||||
* @param HTTPResponse $response
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function applyToResponse($response)
|
||||
{
|
||||
$headers = $this->generateHeadersFor($response);
|
||||
foreach ($headers as $name => $value) {
|
||||
if (!$response->getHeader($name)) {
|
||||
$response->addHeader($name, $value);
|
||||
}
|
||||
}
|
||||
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
|
||||
*
|
||||
* @param HTTPResponse $response
|
||||
* @return array
|
||||
*/
|
||||
public function generateHeadersFor(HTTPResponse $response)
|
||||
{
|
||||
return array_filter([
|
||||
'Last-Modified' => $this->generateLastModifiedHeader(),
|
||||
'Vary' => $this->generateVaryHeader($response),
|
||||
'Cache-Control' => $this->generateCacheHeader(),
|
||||
'Expires' => $this->generateExpiresHeader(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset registered http cache control and force a fresh instance to be built
|
||||
*/
|
||||
public static function reset()
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
// Errors disable cache (unless some errors are cached intentionally by usercode)
|
||||
if ($response->isError()) {
|
||||
// Even if publicCache(true) is specified, errors will be uncacheable
|
||||
$this->disableCache(true);
|
||||
}
|
||||
|
||||
// Don't cache redirects
|
||||
if ($response->isRedirect()) {
|
||||
$this->disableCache(true);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace SilverStripe\Control\RSS;
|
||||
|
||||
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||
@ -226,17 +227,14 @@ class RSSFeed extends ViewableData
|
||||
$response = Controller::curr()->getResponse();
|
||||
|
||||
if (is_int($this->lastModified)) {
|
||||
HTTP::register_modification_timestamp($this->lastModified);
|
||||
HTTPCacheControlMiddleware::singleton()->registerModificationDate($this->lastModified);
|
||||
$response->addHeader("Last-Modified", gmdate("D, d M Y H:i:s", $this->lastModified) . ' GMT');
|
||||
}
|
||||
if (!empty($this->etag)) {
|
||||
HTTP::register_etag($this->etag);
|
||||
$response->addHeader('ETag', "\"{$this->etag}\"");
|
||||
}
|
||||
|
||||
if (!headers_sent()) {
|
||||
HTTP::add_cache_headers();
|
||||
$response->addHeader("Content-Type", "application/rss+xml; charset=utf-8");
|
||||
}
|
||||
$response->addHeader("Content-Type", "application/rss+xml; charset=utf-8");
|
||||
|
||||
SSViewer::config()->update('source_file_comments', $prevState);
|
||||
return $this->renderWith($this->getTemplates());
|
||||
|
@ -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;
|
||||
@ -656,9 +657,6 @@ class RequestHandler extends ViewableData
|
||||
*/
|
||||
public function redirectBack()
|
||||
{
|
||||
// Don't cache the redirect back ever
|
||||
HTTP::set_cache_age(0);
|
||||
|
||||
// Prefer to redirect to ?BackURL, but fall back to Referer header
|
||||
// As a last resort redirect to base url
|
||||
$url = $this->getBackURL()
|
||||
|
@ -128,6 +128,15 @@ class Session
|
||||
*/
|
||||
private static $cookie_secure = false;
|
||||
|
||||
/**
|
||||
* Name of session cache limiter to use.
|
||||
* Defaults to '' to disable cache limiter entirely.
|
||||
*
|
||||
* @see https://secure.php.net/manual/en/function.session-cache-limiter.php
|
||||
* @var string|null
|
||||
*/
|
||||
private static $sessionCacheLimiter = '';
|
||||
|
||||
/**
|
||||
* Session data.
|
||||
* Will be null if session has not been started
|
||||
@ -275,6 +284,11 @@ class Session
|
||||
session_name('SECSESSID');
|
||||
}
|
||||
|
||||
$limiter = $this->config()->get('sessionCacheLimiter');
|
||||
if (isset($limiter)) {
|
||||
session_cache_limiter($limiter);
|
||||
}
|
||||
|
||||
session_start();
|
||||
|
||||
$this->data = isset($_SESSION) ? $_SESSION : array();
|
||||
|
@ -5,8 +5,8 @@ namespace SilverStripe\Forms;
|
||||
use BadMethodCallException;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\HasRequestHandler;
|
||||
use SilverStripe\Control\HTTP;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\Control\NullHTTPRequest;
|
||||
use SilverStripe\Control\RequestHandler;
|
||||
use SilverStripe\Control\Session;
|
||||
@ -868,25 +868,6 @@ class Form extends ViewableData implements HasRequestHandler
|
||||
{
|
||||
$exclude = (is_string($attrs)) ? func_get_args() : null;
|
||||
|
||||
// Figure out if we can cache this form
|
||||
// - forms with validation shouldn't be cached, cos their error messages won't be shown
|
||||
// - forms with security tokens shouldn't be cached because security tokens expire
|
||||
$needsCacheDisabled = false;
|
||||
if ($this->getSecurityToken()->isEnabled()) {
|
||||
$needsCacheDisabled = true;
|
||||
}
|
||||
if ($this->FormMethod() != 'GET') {
|
||||
$needsCacheDisabled = true;
|
||||
}
|
||||
if (!($this->validator instanceof RequiredFields) || count($this->validator->getRequired())) {
|
||||
$needsCacheDisabled = true;
|
||||
}
|
||||
|
||||
// If we need to disable cache, do it
|
||||
if ($needsCacheDisabled) {
|
||||
HTTP::set_cache_age(0);
|
||||
}
|
||||
|
||||
$attrs = $this->getAttributes();
|
||||
|
||||
// Remove empty
|
||||
@ -1568,6 +1549,10 @@ class Form extends ViewableData implements HasRequestHandler
|
||||
*/
|
||||
public function forTemplate()
|
||||
{
|
||||
if (!$this->canBeCached()) {
|
||||
HTTPCacheControlMiddleware::singleton()->disableCache();
|
||||
}
|
||||
|
||||
$return = $this->renderWith($this->getTemplates());
|
||||
|
||||
// Now that we're rendered, clear message
|
||||
@ -1823,4 +1808,30 @@ class Form extends ViewableData implements HasRequestHandler
|
||||
{
|
||||
return FormRequestHandler::create($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the body of this form be cached?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeCached()
|
||||
{
|
||||
if ($this->getSecurityToken()->isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
if ($this->FormMethod() !== 'GET') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't cache if there are required fields, or some other complex validator
|
||||
$validator = $this->getValidator();
|
||||
if ($validator instanceof RequiredFields) {
|
||||
if (count($this->validator->getRequired())) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return 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;
|
||||
@ -379,12 +380,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
$this->original = $this->record;
|
||||
|
||||
// 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']);
|
||||
}
|
||||
|
||||
// Must be called after parent constructor
|
||||
if (!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
|
||||
$this->populateDefaults();
|
||||
|
111
tests/php/Control/HTTPCacheControlIntegrationTest.php
Normal file
111
tests/php/Control/HTTPCacheControlIntegrationTest.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Control\Tests;
|
||||
|
||||
use SilverStripe\Control\HTTP;
|
||||
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\Control\Tests\HTTPCacheControlIntegrationTest\RuleController;
|
||||
use SilverStripe\Control\Tests\HTTPCacheControlIntegrationTest\SessionController;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Dev\FunctionalTest;
|
||||
|
||||
class HTTPCacheControlIntegrationTest extends FunctionalTest
|
||||
{
|
||||
protected static $extra_controllers = [
|
||||
SessionController::class,
|
||||
RuleController::class,
|
||||
];
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
HTTPCacheControlMiddleware::config()
|
||||
->set('defaultState', 'disabled')
|
||||
->set('defaultForcingLevel', 0);
|
||||
HTTPCacheControlMiddleware::reset();
|
||||
}
|
||||
|
||||
public function testFormCSRF()
|
||||
{
|
||||
// CSRF sets caching to disabled
|
||||
$response = $this->get('HTTPCacheControlIntegrationTest_SessionController/showform');
|
||||
$header = $response->getHeader('Cache-Control');
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertNotContains('public', $header);
|
||||
$this->assertNotContains('private', $header);
|
||||
$this->assertContains('no-cache', $header);
|
||||
$this->assertContains('no-store', $header);
|
||||
$this->assertContains('must-revalidate', $header);
|
||||
}
|
||||
|
||||
public function testPublicForm()
|
||||
{
|
||||
// Public forms (http get) allow public caching
|
||||
$response = $this->get('HTTPCacheControlIntegrationTest_SessionController/showpublicform');
|
||||
$header = $response->getHeader('Cache-Control');
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertContains('public', $header);
|
||||
$this->assertContains('must-revalidate', $header);
|
||||
$this->assertNotContains('no-cache', $response->getHeader('Cache-Control'));
|
||||
$this->assertNotContains('no-store', $response->getHeader('Cache-Control'));
|
||||
}
|
||||
|
||||
public function testPrivateActionsError()
|
||||
{
|
||||
// disallowed private actions don't cache
|
||||
$response = $this->get('HTTPCacheControlIntegrationTest_SessionController/privateaction');
|
||||
$header = $response->getHeader('Cache-Control');
|
||||
$this->assertTrue($response->isError());
|
||||
$this->assertContains('no-cache', $header);
|
||||
$this->assertContains('no-store', $header);
|
||||
$this->assertContains('must-revalidate', $header);
|
||||
}
|
||||
|
||||
public function testPrivateActionsAuthenticated()
|
||||
{
|
||||
$this->logInWithPermission('ADMIN');
|
||||
// Authenticated actions are private cache
|
||||
$response = $this->get('HTTPCacheControlIntegrationTest_SessionController/privateaction');
|
||||
$header = $response->getHeader('Cache-Control');
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertContains('private', $header);
|
||||
$this->assertContains('must-revalidate', $header);
|
||||
$this->assertNotContains('no-cache', $header);
|
||||
$this->assertNotContains('no-store', $header);
|
||||
}
|
||||
|
||||
public function testPrivateCache()
|
||||
{
|
||||
$response = $this->get('HTTPCacheControlIntegrationTest_RuleController/privateaction');
|
||||
$header = $response->getHeader('Cache-Control');
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertContains('private', $header);
|
||||
$this->assertContains('must-revalidate', $header);
|
||||
$this->assertNotContains('no-cache', $header);
|
||||
$this->assertNotContains('no-store', $header);
|
||||
}
|
||||
|
||||
public function testPublicCache()
|
||||
{
|
||||
$response = $this->get('HTTPCacheControlIntegrationTest_RuleController/publicaction');
|
||||
$header = $response->getHeader('Cache-Control');
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertContains('public', $header);
|
||||
$this->assertContains('must-revalidate', $header);
|
||||
$this->assertNotContains('no-cache', $header);
|
||||
$this->assertNotContains('no-store', $header);
|
||||
$this->assertContains('max-age=9000', $header);
|
||||
}
|
||||
|
||||
public function testDisabledCache()
|
||||
{
|
||||
$response = $this->get('HTTPCacheControlIntegrationTest_RuleController/disabledaction');
|
||||
$header = $response->getHeader('Cache-Control');
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertNotContains('public', $header);
|
||||
$this->assertNotContains('private', $header);
|
||||
$this->assertContains('no-cache', $header);
|
||||
$this->assertContains('no-store', $header);
|
||||
$this->assertContains('must-revalidate', $header);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Control\Tests\HTTPCacheControlIntegrationTest;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
|
||||
class RuleController extends Controller implements TestOnly
|
||||
{
|
||||
private static $url_segment = 'HTTPCacheControlIntegrationTest_RuleController';
|
||||
|
||||
private static $allowed_actions = [
|
||||
'privateaction',
|
||||
'publicaction',
|
||||
'disabledaction',
|
||||
];
|
||||
|
||||
protected function init()
|
||||
{
|
||||
parent::init();
|
||||
// Prefer public by default
|
||||
HTTPCacheControlMiddleware::singleton()->publicCache();
|
||||
}
|
||||
|
||||
public function privateaction()
|
||||
{
|
||||
HTTPCacheControlMiddleware::singleton()->privateCache();
|
||||
return 'private content';
|
||||
}
|
||||
|
||||
public function publicaction()
|
||||
{
|
||||
HTTPCacheControlMiddleware::singleton()
|
||||
->publicCache()
|
||||
->setMaxAge(9000);
|
||||
return 'public content';
|
||||
}
|
||||
|
||||
public function disabledaction()
|
||||
{
|
||||
HTTPCacheControlMiddleware::singleton()->disableCache();
|
||||
return 'uncached content';
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Control\Tests\HTTPCacheControlIntegrationTest;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\FormAction;
|
||||
use SilverStripe\Forms\TextField;
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\Security\SecurityToken;
|
||||
|
||||
class SessionController extends Controller implements TestOnly
|
||||
{
|
||||
private static $url_segment = 'HTTPCacheControlIntegrationTest_SessionController';
|
||||
|
||||
private static $allowed_actions = [
|
||||
'showform',
|
||||
'privateaction',
|
||||
'publicaction',
|
||||
'showpublicform',
|
||||
'Form',
|
||||
];
|
||||
|
||||
protected function init()
|
||||
{
|
||||
parent::init();
|
||||
// Prefer public by default
|
||||
HTTPCacheControlMiddleware::singleton()->publicCache();
|
||||
}
|
||||
|
||||
public function getContent()
|
||||
{
|
||||
return '<p>Hello world</p>';
|
||||
}
|
||||
|
||||
public function showform()
|
||||
{
|
||||
// Form should be set to private due to CSRF
|
||||
SecurityToken::enable();
|
||||
return $this->renderWith('BlankPage');
|
||||
}
|
||||
|
||||
public function showpublicform()
|
||||
{
|
||||
// Public form doesn't use CSRF and thus no session usage
|
||||
SecurityToken::disable();
|
||||
return $this->renderWith('BlankPage');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @throws \SilverStripe\Control\HTTPResponse_Exception
|
||||
*/
|
||||
public function privateaction()
|
||||
{
|
||||
if (!Permission::check('ANYCODE')) {
|
||||
$this->httpError(403, 'Not allowed');
|
||||
}
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
public function publicaction()
|
||||
{
|
||||
return 'Hello!';
|
||||
}
|
||||
|
||||
public function Form()
|
||||
{
|
||||
$form = new Form(
|
||||
$this,
|
||||
'Form',
|
||||
new FieldList(new TextField('Name')),
|
||||
new FieldList(new FormAction('submit', 'Submit'))
|
||||
);
|
||||
$form->setFormMethod('GET');
|
||||
return $form;
|
||||
}
|
||||
}
|
@ -7,8 +7,8 @@ use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\HTTP;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Core\Kernel;
|
||||
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
use SilverStripe\Control\Session;
|
||||
use SilverStripe\Dev\FunctionalTest;
|
||||
|
||||
/**
|
||||
@ -18,31 +18,49 @@ use SilverStripe\Dev\FunctionalTest;
|
||||
*/
|
||||
class HTTPTest extends FunctionalTest
|
||||
{
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
// Set to disabled at null forcing level
|
||||
HTTPCacheControlMiddleware::config()
|
||||
->set('defaultState', 'disabled')
|
||||
->set('defaultForcingLevel', 0);
|
||||
HTTPCacheControlMiddleware::reset();
|
||||
}
|
||||
|
||||
public function testAddCacheHeaders()
|
||||
{
|
||||
$body = "<html><head></head><body><h1>Mysite</h1></body></html>";
|
||||
$response = new HTTPResponse($body, 200);
|
||||
$this->assertEmpty($response->getHeader('Cache-Control'));
|
||||
HTTPCacheControlMiddleware::singleton()->publicCache();
|
||||
HTTPCacheControlMiddleware::singleton()->setMaxAge(30);
|
||||
|
||||
HTTP::set_cache_age(30);
|
||||
|
||||
HTTP::add_cache_headers($response);
|
||||
$this->addCacheHeaders($response);
|
||||
$this->assertNotEmpty($response->getHeader('Cache-Control'));
|
||||
|
||||
// Ensure max-age is zero for development.
|
||||
/** @var Kernel $kernel */
|
||||
$kernel = Injector::inst()->get(Kernel::class);
|
||||
$kernel->setEnvironment(Kernel::DEV);
|
||||
// Ensure cache headers are set correctly when disabled via config (e.g. when dev)
|
||||
HTTPCacheControlMiddleware::config()
|
||||
->set('defaultState', 'disabled')
|
||||
->set('defaultForcingLevel', HTTPCacheControlMiddleware::LEVEL_DISABLED);
|
||||
HTTPCacheControlMiddleware::reset();
|
||||
HTTPCacheControlMiddleware::singleton()->publicCache();
|
||||
HTTPCacheControlMiddleware::singleton()->setMaxAge(30);
|
||||
$response = new HTTPResponse($body, 200);
|
||||
HTTP::add_cache_headers($response);
|
||||
$this->assertContains('max-age=0', $response->getHeader('Cache-Control'));
|
||||
$this->addCacheHeaders($response);
|
||||
$this->assertContains('no-cache', $response->getHeader('Cache-Control'));
|
||||
$this->assertContains('no-store', $response->getHeader('Cache-Control'));
|
||||
$this->assertContains('must-revalidate', $response->getHeader('Cache-Control'));
|
||||
|
||||
// Ensure max-age setting is respected in production.
|
||||
$kernel->setEnvironment(Kernel::LIVE);
|
||||
HTTPCacheControlMiddleware::config()
|
||||
->set('defaultState', 'disabled')
|
||||
->set('defaultForcingLevel', 0);
|
||||
HTTPCacheControlMiddleware::reset();
|
||||
HTTPCacheControlMiddleware::singleton()->publicCache();
|
||||
HTTPCacheControlMiddleware::singleton()->setMaxAge(30);
|
||||
$response = new HTTPResponse($body, 200);
|
||||
HTTP::add_cache_headers($response);
|
||||
$this->assertContains('max-age=30', explode(', ', $response->getHeader('Cache-Control')));
|
||||
$this->addCacheHeaders($response);
|
||||
$this->assertContains('max-age=30', $response->getHeader('Cache-Control'));
|
||||
$this->assertNotContains('max-age=0', $response->getHeader('Cache-Control'));
|
||||
|
||||
// Still "live": Ensure header's aren't overridden if already set (using purposefully different values).
|
||||
@ -51,39 +69,45 @@ class HTTPTest extends FunctionalTest
|
||||
'Pragma' => 'no-cache',
|
||||
'Cache-Control' => 'max-age=0, no-cache, no-store',
|
||||
);
|
||||
$response = new HTTPResponse($body, 200);
|
||||
foreach ($headers as $name => $value) {
|
||||
$response->addHeader($name, $value);
|
||||
foreach ($headers as $header => $value) {
|
||||
$response->addHeader($header, $value);
|
||||
}
|
||||
HTTP::add_cache_headers($response);
|
||||
foreach ($headers as $name => $value) {
|
||||
$this->assertEquals($value, $response->getHeader($name));
|
||||
HTTPCacheControlMiddleware::reset();
|
||||
HTTPCacheControlMiddleware::singleton()->publicCache();
|
||||
HTTPCacheControlMiddleware::singleton()->setMaxAge(30);
|
||||
$this->addCacheHeaders($response);
|
||||
foreach ($headers as $header => $value) {
|
||||
$this->assertEquals($value, $response->getHeader($header));
|
||||
}
|
||||
}
|
||||
|
||||
public function testConfigVary()
|
||||
{
|
||||
/** @var Kernel $kernel */
|
||||
$kernel = Injector::inst()->get(Kernel::class);
|
||||
$body = "<html><head></head><body><h1>Mysite</h1></body></html>";
|
||||
$response = new HTTPResponse($body, 200);
|
||||
$kernel->setEnvironment(Kernel::LIVE);
|
||||
HTTP::set_cache_age(30);
|
||||
HTTP::add_cache_headers($response);
|
||||
HTTPCacheControlMiddleware::singleton()
|
||||
->setMaxAge(30)
|
||||
->setVary('X-Requested-With, X-Forwarded-Protocol');
|
||||
$this->addCacheHeaders($response);
|
||||
|
||||
// Vary set properly
|
||||
$v = $response->getHeader('Vary');
|
||||
$this->assertNotEmpty($v);
|
||||
|
||||
$this->assertContains("Cookie", $v);
|
||||
$this->assertContains("X-Forwarded-Protocol", $v);
|
||||
$this->assertContains("User-Agent", $v);
|
||||
$this->assertContains("Accept", $v);
|
||||
$this->assertContains("X-Requested-With", $v);
|
||||
$this->assertNotContains("Cookie", $v);
|
||||
$this->assertNotContains("User-Agent", $v);
|
||||
$this->assertNotContains("Accept", $v);
|
||||
|
||||
HTTP::config()->update('vary', '');
|
||||
// No vary
|
||||
HTTPCacheControlMiddleware::singleton()
|
||||
->setMaxAge(30)
|
||||
->setVary(null);
|
||||
HTTPCacheControlMiddleware::reset();
|
||||
HTTPCacheControlMiddleware::config()
|
||||
->set('defaultVary', []);
|
||||
|
||||
$response = new HTTPResponse($body, 200);
|
||||
HTTP::add_cache_headers($response);
|
||||
|
||||
$this->addCacheHeaders($response);
|
||||
$v = $response->getHeader('Vary');
|
||||
$this->assertEmpty($v);
|
||||
}
|
||||
@ -370,4 +394,23 @@ class HTTPTest extends FunctionalTest
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process cache headers on a response
|
||||
*
|
||||
* @param HTTPResponse $response
|
||||
*/
|
||||
protected function addCacheHeaders(HTTPResponse $response)
|
||||
{
|
||||
// Mock request
|
||||
$session = new Session([]);
|
||||
$request = new HTTPRequest('GET', '/');
|
||||
$request->setSession($session);
|
||||
|
||||
// Run middleware
|
||||
HTTPCacheControlMiddleware::singleton()
|
||||
->process($request, function (HTTPRequest $request) use ($response) {
|
||||
return $response;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Control\Tests\Middleware;
|
||||
|
||||
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
|
||||
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();
|
||||
$this->assertTrue($this->isDisabled($hcc), 'caching starts as disabled');
|
||||
|
||||
$hcc->enableCache();
|
||||
$this->assertFalse($this->isDisabled($hcc));
|
||||
|
||||
$hcc->publicCache();
|
||||
$this->assertTrue($this->isPublic($hcc), 'public can be set at start');
|
||||
|
||||
$hcc->privateCache();
|
||||
$this->assertTrue($this->isPrivate($hcc), 'private overrides public');
|
||||
|
||||
$hcc->publicCache();
|
||||
$this->assertFalse($this->isPublic($hcc), 'public does not overrides private');
|
||||
|
||||
$hcc->disableCache();
|
||||
$this->assertTrue($this->isDisabled($hcc), 'disabled overrides private');
|
||||
|
||||
$hcc->privateCache();
|
||||
$this->assertFalse($this->isPrivate($hcc), 'private does not override disabled');
|
||||
|
||||
$hcc->enableCache(true);
|
||||
$this->assertFalse($this->isDisabled($hcc));
|
||||
|
||||
$hcc->publicCache(true);
|
||||
$this->assertTrue($this->isPublic($hcc), 'force-public overrides disabled');
|
||||
|
||||
$hcc->privateCache();
|
||||
$this->assertFalse($this->isPrivate($hcc), 'private does not overrdie force-public');
|
||||
|
||||
$hcc->privateCache(true);
|
||||
$this->assertTrue($this->isPrivate($hcc), 'force-private overrides force-public');
|
||||
|
||||
$hcc->publicCache(true);
|
||||
$this->assertFalse($this->isPublic($hcc), 'force-public does not override force-private');
|
||||
|
||||
$hcc->disableCache(true);
|
||||
$this->assertTrue($this->isDisabled($hcc), 'force-disabled overrides force-private');
|
||||
|
||||
$hcc->publicCache(true);
|
||||
$this->assertFalse($this->isPublic($hcc), 'force-public does not overrides force-disabled');
|
||||
}
|
||||
|
||||
protected function isPrivate(HTTPCacheControlMiddleware $hcc)
|
||||
{
|
||||
return $hcc->hasDirective('private') && !$hcc->hasDirective('public') && !$hcc->hasDirective('no-cache');
|
||||
}
|
||||
|
||||
protected function isPublic(HTTPCacheControlMiddleware $hcc)
|
||||
{
|
||||
return $hcc->hasDirective('public') && !$hcc->hasDirective('private') && !$hcc->hasDirective('no-cache');
|
||||
}
|
||||
|
||||
protected function isDisabled(HTTPCacheControlMiddleware $hcc)
|
||||
{
|
||||
return $hcc->hasDirective('no-cache') && !$hcc->hasDirective('private') && !$hcc->hasDirective('public');
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace SilverStripe\Core\Tests\Startup;
|
||||
|
||||
use SilverStripe\Control\Cookie;
|
||||
use SilverStripe\Control\HTTPApplication;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
|
Loading…
x
Reference in New Issue
Block a user