15 KiB
4.2.0
Overview
- Disable session-based stage setting in
Versioned
(see #1578) - Deprecated
FunctionalTest::useDraftSite()
. You should use querystring args instead for setting stage.
Upgrading
App folder name
The standard 'mysite' code naming convention has changed in 4.2. Although existing sites can continue
to use 'mysite/codeto store their base project code, the recommendation and new default is to store code in
app/src`.
Additionally, we reinforce the recommendation to use psr-4 autoloading in your project to speed up class loading.
In order to upgrade a site to use app/src
folder:
- Rename the folder
mysite
toapp
andcode
tosrc
. - Update your
app/_config/mysite.yml
config to the below:
---
Name: myproject
---
SilverStripe\Core\Manifest\ModuleManifest:
project: app
- add psr-4 for your root project files and namespace. An example
composer.json
below shows how this might look:
{
"autoload": {
"psr-4": {
"TractorCow\\MyWebsite\\": "app/src/"
},
"classmap": [
"app/src/Page.php",
"app/src/PageController.php"
]
}
}
- Ensure you flush your site with
?flush=all
Note: In 5.0 the app folder will be fixed to app
and cannot be soft-coded via mysite.yml
Disable session-based stage setting
When viewing a versioned record (usually pages) in "draft" mode, SilverStripe used to record this mode in the session for further requests. This has the advantage of transparently working on XHR and API requests, as well as authenticated users navigating through other views.
These subsequent requests no longer carried an explicit stage
query parameter,
which meant the same URL might show draft or live content depending on your session state.
While most HTTP caching layers deal gracefully with this variation by disabling
any caching when a session cookie is present, there is a small chance
that draft content is exposed to unauthenticated users for the lifetime of the cache.
Due to this potential risk for information leakage,
we have decided to only rely on the stage
query parameter.
If you are consistently using the built-in SiteTree->Link()
and Controller->Link()
methods to get URLs, this change likely won't affect you.
If you are manually concatenating URLs to SilverStripe controllers
rather than through their Link()
methods (in custom PHP or JavaScript),
or have implemented your own Link()
methods on controllers exposing
versioned objects, you'll need to check your business logic.
Alternatively, you can opt-out of this security feature via YAML configuration:
SilverStripe\Versioned\Versioned:
use_session: true
Check our versioning docs for more details.
New Versioned API
The following methods have been added to [api:SilverStripe\Versioned\Versioned] class:
withVersionedMode()
Allows users to execute a closure which may internally modify the current stage, but will guarantee these changes are reverted safely on return. Helpful when temporarily performing a task in another stage or view mode.get_draft_site_secured()
/set_draft_site_secured()
Enables the explicit toggle of draft site security. By setting this to false, you can expose a draft mode to unauthenticated users. ReplacesunsecuredDraftSite
session var.get_default_reading_mode()
/set_default_reading_mode()
The default reading mode is now configurable. Any non-default reading mode must have querystring args to be visible. This will be the mode choosen for requests that do not have these args. Note that the default mode for CMS is now draft, but is live on the frontend.
A new class [api:SilverStripe\Versioned\ReadingMode] has also been added to assist with conversion of the reading mode between:
- Reading mode string
- DataQuery parameters
- Querystring parameters
Link tracking
SiteTreeLinkTracking has been split and refactored into two extensions, and now
no longer applies exclusively to HTMLContent
areas on SiteTree
objects, but now
all DataObject
classes.
SiteTreeLinkTracking
-> Tracks links between any object and SiteTree objects, generated from [sitetree_link] shortcodes in html areas.FileLinkTracking
-> Tracks links between any object and File objects, generated from [image] and [file_link] shortcodes in html areas.
Note that the ImageTracking
property has been deprecated in favour of FileTracking
,
which includes and tracks non-image files as well.
By default HasBrokenFile
and HasBrokenLink
properties are still supported, but
only for SiteTree
objects by default. Non-SiteTree objects will still have
both FileTracking
and LinkTracking
relations available for tracking
linked records.
In addition, File::BackLinkTracking()
and SiteTree::BackLinkTracking()
are now polymorphic,
and may now both contain non-SiteTree objects. Polymorphic many_many through relations are
currently experimentally supported.
User code which relies on SiteTree-only results for these properties will need to be updated to consider other types.
Additionally, the SiteTree_LinkTracking
and SiteTree_ImageTracking
tables no longer exist,
and are replaced by the SiteTreeLink
and FileLink
many_many through joining classes instead.
Code which relies on raw SQL queries to these tables will need to be updated.
SiteTreeFileExtension
is deprecated, and has it's functionality baked directly into File
dataobject.
New upgrader commands
Two new commands have been added to the SilverStripe upgrader tool: environment
and reorganise
.
environment
allows you to convert your _ss_environment.php
file to an equivalent .env
file when migrating a SilverStripe 3 project to SilverStripe 4.
reorganise
renames your mysite
and mysite/code
folders to app
and app/src
. It also warns you of any occurence of mysite
in your codebase.
cd ~/my-project-root
upgrade-code environment --write
upgrade-code reorganise --write
New GridField Action Menu
A new GridField_ActionMenu
is included by default in GridFields configured with GridFieldConfig_RecordEditor
or GridFieldConfig_RelationEditor
.
In addition to this GridFieldDeleteAction
and GridFieldEditButton
now implement GridField_ActionMenuItem
,
this means that any GridField that uses a config of or based on GridFieldConfig_RecordEditor
or GridFieldConfig_RelationEditor
will have an action menu on each item row with
the 'Delete/Unlink' and 'Edit' actions moved into it.
If you wish to opt out of having this menu and the respective actions moved into it, you can remove the GridField_ActionMenu
component from the config that is passed into your GridField.
// method 1: removing GridField_ActionMenu from a new GridField
$config = GridFieldConfig_RecordEditor::create();
$config->removeComponentsByType(GridField_ActionMenu);
$gridField = new GridField('Teams', 'Teams', $this->Teams(), $config);
// method 2: removing GridField_ActionMenu from an existing GridField
$gridField->getConfig()->removeComponentsByType(GridField_ActionMenu);
Versioned cache segmentation
SilverStripe\Core\Cache\CacheFactory
now maintains separate cache pools for each versioned stage. This prevents developers from caching draft data and then accidentally exposing it on the live stage without potentially required authorisation checks. Unless you rely on caching across stages, you don't need to change your own code for this change to take effect. Note that cache keys will be internally rewritten, causing any existing cache items to become invalid when this change is deployed.
// Before:
$cache = Injector::inst()->get(CacheInterface::class . '.myapp');
Versioned::set_stage(Versioned::DRAFT);
$cache->set('my_key', 'Some draft content. Not for public viewing yet.');
Versioned::set_stage(Versioned::LIVE);
$cache->get('my_key'); // 'Some draft content. Not for public viewing yet'
// After:
$cache = Injector::inst()->get(CacheInterface::class . '.myapp');
Versioned::set_stage(Versioned::DRAFT);
$cache->set('my_key', 'Some draft content. Not for public viewing yet.');
Versioned::set_stage(Versioned::LIVE);
$cache->get('my_key'); // null
Data that is not content sensitive can be cached across stages by simply opting out of the segmented cache with the disable-container
argument.
SilverStripe\Core\Injector\Injector:
Psr\SimpleCache\CacheInterface.myapp:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: "MyInsensitiveData"
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, 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 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:
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
).
<?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 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
-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 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 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.
<?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 existingCache-Control: no-store
defaults instead - Removed
Vary: Accept
, since it's very uncommon to vary content on theAccept
headers submitted through the request, and it can significantly decrease the likelyhood of a cache hit. Note this is different fromVary: 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 inDataObject::__construct()
. UsesETag
calculation based on response body which is more accurate, and resilient against partial and object caching which can produce staleLast-Modified
values. - Deprecated
HTTP::add_cache_headers()
. Headers are added automatically byHTTPCacheControlMiddleware
instead. - Deprecated
HTTP::set_cache_age()
. UseHTTPCacheControlMiddleware::singleton()->setMaxAge($age)
- Deprecated
HTTP.cache_ajax_requests
. UseHTTPCacheControlMiddleware::disableCache()
instead - Deprecated
HTTP.modification_date
. Handled byHTTPCacheControlMiddleware
- Deprecated
HTTP.disable_http_cache
. UseHTTPCacheControlMiddleware.defaultState
anddefaultForcingLevel
instead - Deprecated
HTTP::register_modification_date()
. UseHTTPCacheControlMiddleware::registerModificationDate()
instead - Deprecated
HTTP::register_modification_timestamp()
. UseHTTPCacheControlMiddleware::registerModificationDate()
instead - Deprecated
HTTP::register_etag()
. UseHTTPCacheControlMiddleware::ETagMiddleware()
instead