title |
---|
5.2.0 (unreleased) |
- Security considerations
- Features and enhancements
- New ORM features
- GridField components now work with arbitrary data
- ErrorPage allowed codes configuration
- Create random passwords for new users
- Buttons to select all files and deselect all files
- New searchable dropdown fields
- New
UrlField
- Create file variants with different extensions
- More nuanced permissions for
/dev/*
routes - Generic typehints
- New exception in React forms
- Other new features
- API changes
- Bug fixes
This release includes several security fixes. Review the individual vulnerability disclosure for more detailed descriptions of each security fix. We highly encourage upgrading your project to include the latest security patches.
We have provided a severity rating of the vulnerabilities below based on the CVSS score. Note that the impact of each vulnerability could vary based on the specifics of each project. You can read the severity rating definitions in the Silverstripe CMS release process.
- CVE-2023-49783 - No permission checks for editing or deleting records with CSV import form Severity: Medium
- If you implement a custom subclass of
BulkLoader
or you're using aBulkLoader
directly in your code, there may be additional actions you need to take to secure your project.
- If you implement a custom subclass of
- CVE-2023-48714 - Record titles for restricted records can be viewed if exposed by GridFieldAddExistingAutocompleter Severity: Medium
- CVE-2023-44401 - View permissions are bypassed for paginated lists of ORM data in GraphQL queries Severity: Medium
This release comes jampacked with new ORM features, granting you access to some new abstractions for more powerful and efficient queries.
Traditionally, if you wanted to have multiple has_many
relations for the same class, you would have to include a separate has_one
relation for each has_many
relation.
This release includes a new has_one
syntax to declare that your has_one
should be allowed to handle multiple reciprocal has_many
relations. The syntax for that is as follows:
[hint]
Multi-relational has_one
relations must be polymorphic.
[/hint]
namespace App\Model;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectSchema;
class MyExample extends DataObject
{
// ...
private static array $has_one = [
'MyMultiRelationalRelation' => [
// The class here is the class for the has_one - it must be polymorphic.
'class' => DataObject::class,
// Setting this to true is what defines this has_one relation as multi-relational
DataObjectSchema::HAS_ONE_MULTI_RELATIONAL => true,
],
];
}
Multiple has_many
relations on a single class can point to the above has_one
relation, and they will be correctly saved and resolved when you get the relation list.
[warning]
This new feature means sometimes the value in the associative has_one
configuration array will be an array, rather than a class name.
If you are relying on fetching this configuration to find the class names of has_one
relations, consider using
DataObject::hasOne()
or DataObjectSchema::hasOneComponent()
instead.
[/warning]
See multi-relational has_one
in the relations docs for more details about this relation type.
Abstractions for the SQL UNION
clause have been added to SQLSelect
and DataQuery
.
To add a UNION
clause to an SQLSelect
, call the SQLSelect::addUnion()
method and pass in the SQLSelect
query you want to combine using a union.
For the second argument, you can leave it blank for a default union (which is functionality equivalent to a distinct union in MySQL) - or you can pass in one of the SQLSelect::UNION_ALL
or SQLSelect::UNION_DISTINCT
constants for a UNION ALL
or UNION DISTINCT
clause respectively.
$baseQuery = SQLSelect::create()->setFrom($tableName)->addWhere(...);
$anotherQuery = SQLSelect::create()->setFrom($tableName)->addWhere(...);
$baseQuery->addUnion($anotherQuery, SQLSelect::UNION_DISTINCT);
To add a UNION
clause to an DataQuery
, call the DataQuery::union()
method and pass in either another DataQuery
or an SQLSelect
query you want to combine using a union. The same constants used for SQLSelect
can be passed in here as well.
$baseQuery = DataQuery::create(MyClass::class)->where(...);
$anotherQuery = DataQuery::create(MyClass::class)->where(...);
$baseQuery->union($anotherQuery, SQLSelect::UNION_ALL);
Abstractions for Common Table Expressions (aka the SQL WITH
clause) have been added to SQLSelect
and DataQuery
.
Common Table Expressions are a powerful tool both for optimising complex queries, and for creating recursive queries. This functionality is abstracted in the SQLSelect::addWith()
and DataQuery::with()
methods.
Older database servers don't support this functionality, and the core implementation is only valid for MySQL, so if you are using this functionality in an open source module or a project that you can't guarantee the type and version of database being used, you should wrap the query in a condition checking if CTEs are supported. You can do that by calling DB::get_conn()->supportsCteQueries()
.
Check out the SQL Queries and Data Model and ORM documentation for usage details and examples.
Abstractions for RIGHT JOIN
have been added with the new DataList::rightJoin()
, DataQuery::rightJoin()
, and SQLConditionalExpression::addRightJoin()
methods. The signature for these methods is identical to their LEFT JOIN
and INNER JOIN
counterparts.
Previously the SQLConditionalExpression
abstraction couldn't handle multiple table names being passed into its FROM
clause. This restriction has been removed, so you can now have queries selecting from multiple tables so long as your database supports it. If you were working around that limitation by adding an explicit comma to subsequent tables in the FROM
clause for your queries, you'll need to remove the comma.
You can also now choose to not have a FROM
clause in an SQLSelect
query, which can be useful for setting up simple queries to be used in unit tests.
When using DataQuery
, it is possible to use collations and other raw SQL field statements as part of the query's SELECT
clause. If these have an alias that matches the name of an existing database column, this results in an exception being thrown.
You can choose to allow those conflicts to be resolved via a CASE
statement. In that scenario, if the value in the database column is null, the value for your custom field statement will be used. This is enabled per query by passing true
to the new DataQuery::setAllowCollidingFieldStatements()
method.
$query = new DataQuery(MyClass::class);
$query->selectField('"my custom title" AS "Title"');
$query->setAllowCollidingFieldStatements(true);
It has historically been difficult to use a GridField
to display data that isn't represented by DataObject
records - and even more difficult to edit that data.
We have removed several barriers to using the GridField
to display arbitrary data. Descriptive exceptions will be thrown when specific information cannot be dynamically identified, such as which columns to display and what form fields to use when viewing or editing data. Note that these new exceptions do not break backwards compatibility. Any scenario that will throw an exception now would have already done so - but the old exception would not have been sufficiently descriptive to quickly understand what changes are required to get a functioning GridField
.
This applies to all GridFieldComponent
classes in silverstripe/framework
except for GridFieldAddExistingAutocompleter
and GridFieldLevelup
, which both explicitly require the model class for the associated GridField
to be a subclass of DataObject
.
See using GridField
with arbitrary data for more information.
By default, all available error codes are present in the dropdown in the CMS. This can be overwhelming and there are a few (looking at you, 418) that can
be confusing. To that end, you can limit the codes in the dropdown with the config value allowed_error_codes
like so:
SilverStripe\ErrorPage\ErrorPage:
allowed_error_codes:
- 400
- 403
- 404
- 500
If a new user is created in the security section of the CMS with an empty password, then a strong random password will now be automatically assigned to the user which will not be visible to the person creating the user. The user will need to click the "I've forgotten my password" link in order to receive a password reset email so they can then enter a new password.
This is a behavioural change from the change introduced in Silverstripe CMS 5.1 where blank passwords were disallowed when creating a new user. This change in 5.1 meant the administrator setting up the user could know what the users password was until the user changed it. Prior to 5.1, blank passwords were allowed though users were unable to log in using them when the default Silverstripe member authenticator was used, thus forcing the user to reset their password.
This only applies to users created through the security section of the CMS. Users created programatically with an empty password will not have a random password automatically assigned to them. This is the current behaviour and is often used for non-production purposes such as unit-testing.
The files section of the CMS now has buttons to select and deselect all files and folders currently on the screen, which is useful when you want to perform bulk operations. These buttons show at the bottom of the screen in the bulk actions toolbar. The deselect all button also shows the number of currently selected items. As these buttons are on the bulk actions toolbar they are only visible if at least one item has been selected.
A pair of new dropdown form fields have been added which are particularly useful for dropdowns with a large number of options.
SearchableDropdownField
is similar to DropdownField
which allows selecting a single option, and SearchableMultiDropdownField
is similar to ListboxField
which allows selecting multiple options.
Both of these fields include a setIsLazyLoaded()
method which will load a limited number of options at a time using an AJAX request matching what a user has typed in. There are quite a few options to customise these, including optionally using SearchContext
to power the lazy-loaded search functionality. We encourage you to look at the API documentation for these new classes to see what you can do with them.
Note that these are both powered by react components, and are only intended to be used within the CMS. If you want to use them on the front-end of your project you will need to provide your own templates and JavaScript implementation for them.
A new UrlField
has been added which is a subclass of TextField
with some additional validation rules. It will validate that the value entered is a valid absolute URL with a protocol and a host.
A SearchableDropdownField
will now be used when automatically scaffolding has_one
relations on a form in the CMS. Previously a DropdownField
was used, and when there were over 100 items a NumericField
was used which was not user friendly.
Previously the DBForeignKey.dropdown_field_threshold
config property was used as the threshold of the number of options to decide when to switch between auto-scaffolding a DropdownField
and a NumericField
. This configuration property is now used as the threshold of the number of options to decide when to start using lazy-loading for the SearchableDropdownField
.
A low-level API has been added which allows the creation of file variants with a different extension than the original extension. A file variant is a manipulated version of the original file - for example if you resize an image or convert a file to another format, this will generate a variant (leaving the original file intact).
Some examples of when you might want this are:
- Generating thumbnails for videos, documents, etc
- Converting images to
.webp
for faster page load times - Converting documents to
.pdf
so downloaded documents are more portable
See file manipulation for details about how to use this new API.
Previously, all /dev/*
routes registered with DevelopmentAdmin
(for example /dev/tasks/MyBuildTask
) could only be access by administrator users, and this couldn't be configured.
Now, all of the controllers which handle these routes that come packaged in a core or supported module have a new init_permissions
configuration property (e.g. TaskRunner.init_permissions
). This new configuration can be used to grant non-administrative users access to these routes.
You can also now optionally implement a canView()
method on your BuildTask
implementations to restrict accessed for specific tasks even further. This means you can grant access to some tasks to specific users or groups without granting access to all tasks.
Typehints using PHPStan-style generic types have been added to PHPDocs in many areas of the codebase of supported modules. The primary goal of this is to improve the developer experience by correctly reporting to your IDE what types it should expect, for example when looping through a DataList
. In many cases your IDE will now know what types to expect without needing you to prompt it with @var
annotation comments.
[info] There are some cases where this goal conflicts with having types that are correctly identified by PHPStan itself (or other static analysis tools). For example conditional return types aren't supported as widely in IDEs as generics themselves are, so we opted to not use conditional return types even when those would result in a more accurate type for static analysis tools. [/info]
While you should see some improvements immediately after updating, there are some changes you can make to your own codebase to best use the new generic type hints.
See Generics By Examples | PHPStan and Generics in PHP using PHP DocComments | DEVSENSE for more information about PHP generic typehints.
In your project code, any time you return an instance of SS_List
(such as a DataList
or ArrayList
), you can add a generic typehint to declare what kind of object the returned list contains. This example will hint to the IDE that it returns a DataList
containing CarouselItem
records:
use App\Model\CarouselItem;
use SilverStripe\ORM\DataList;
/**
* @return DataList<CarouselItem>
*/
function getCarouselItems(): DataList
{
return CarouselItem::get();
}
The generic typing on the Extension
class can be used to tell your IDE what type to expect for the $owner
property and getOwner()
method.
For this to be useful, you need to tell your IDE that your subclass @extends
the Extension
class, and tell it what type the owner should be.
[warning]
Don't forget to include a use
statement, even if you're not explicitly referencing the type anywhere in your actual code. Your IDE needs the use
statement to resolve the FQCN for the class you're referencing in the typehint.
[/warning]
namespace App\Extension;
use SilverStripe\Core\Extension;
use SilverStripe\SiteConfig\SiteConfig;
/**
* @extends Extension<SiteConfig>
*/
class SiteConfigExtension extends Extension
{
// ...
}
This is also a useful way to indicate to developers at a glance what type(s) the extension is designed to be applied to.
For example you might have an extension that can apply to any SiteTree
class, or to LeftAndMain
and GridFieldDetailForm_ItemRequest
classes, which you can indicate using a union typehint: @extends Extension<LeftAndMain|GridFieldDetailForm_ItemRequest>
.
If you use the data()
method or the $dataRecord
property in your page controllers, you may find it useful for your IDE to know specifically what page class that data represents.
For this to work, you need to make sure your base PageController
class has a @template
type to extend. Any time you use @extends
, the class being extended needs to have a @template
type so that your IDE knows what the type you're passing in is going to be used for.
namespace {
use SilverStripe\CMS\Controllers\ContentController;
/**
* @template T of Page
* @extends ContentController<T>
*/
class PageController extends ContentController
{
// ...
}
}
namespace App\PageType;
use PageController;
/**
* @extends PageController<HomePage>
*/
class HomepageController extends PageController
{
// ...
}
A LogicException
is now thrown by FormSchema::getSchema()
if a react component was not found for a field type.
[warning] If your project or a module you're using is currently trying to include a field which doesn't have a react component into a react-rendered form, it will have been silently failing. The form will have been rendering everything except for the field(s) which have no react component.
This will now fail by throwing an exception, which means your form won't render at all until you remove or replace the field(s). [/warning]
- A new
SiteTree.hide_pagetypes
configuration property has been added. UnlikeSiteTree.hide_ancestor
(which has now been deprecated), this is an array. This allows you to define all page types that should be hidden in a single configuration property in your YAML configuration. - A new
DBText.summary_sentence_separators
configuration property has been added. This is used to split sentences in theDBText::Summary()
method. The default configuration value includes.
,!
, and?
. Previously, only.
was used to split sentences. - You can now upload Braille ASCII file format (
.brf
) files to the CMS without needing to modify the allowed file types. - The assets admin section now respects the
Upload_Validator.default_max_file_size
configuration ifAssetAdmin.max_upload_size
has not been explicitly set for your project. Previously, asset admin would ignoreUpload_Validator.default_max_file_size
and just use the PHPupload_max_filesize
ini configuration by default. - Redirector pages with a link to a missing or unpublished page/file will now return a 404 response. You can revert to the previous behaviour by setting the
RedirectorPageController.missing_redirect_is_404
configuration property tofalse
.
The following legacy subclasses of PasswordEncryptor
have been deprecated, and will be removed in a future major release. If you are using one of these password encryptors in your projects, we strongly recommend swapping to one that has not been deprecated (PasswordEncryptor_Blowfish
is the current recommendation, and is the default encryptor for passwords in new installations). Note that changing the password encryptor will also require that all of your members reset their passwords.
PasswordEncryptor_None
PasswordEncryptor_LegacyPHPHash
PasswordEncryptor_MySQLOldPassword
PasswordEncryptor_MySQLPassword
The following unused API have been deprecated and will be removed in a future major release:
- Configuration property
DataObject.subclass_access
- Public static method
DataObject::disable_subclass_access()
- Public static method
DataObject::enable_subclass_access()
The ViewableData::getIterator()
method has been deprecated and will be removed in a future major release.
The SiteTree.hide_ancestor
configuration property has been deprecated. Use SiteTree.hide_pagetypes
instead.
The $having
parameter in the Versioned::Versions()
method has been deprecated. This parameter was never used, and has been removed from the method signature.
This release includes a number of bug fixes to improve a broad range of areas. Check the change logs for full details of these fixes split by module. Thank you to the community members that helped contribute these fixes as part of the release!