diff --git a/en/02_Developer_Guides/00_Model/11_Scaffolding.md b/en/02_Developer_Guides/00_Model/11_Scaffolding.md index 88c8a0225..6f16442e7 100644 --- a/en/02_Developer_Guides/00_Model/11_Scaffolding.md +++ b/en/02_Developer_Guides/00_Model/11_Scaffolding.md @@ -25,6 +25,7 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static $db = [ 'IsActive' => 'Boolean', 'Title' => 'Varchar', @@ -226,9 +227,10 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static $searchable_fields = [ - 'Name', - 'ProductCode', + 'Name', + 'ProductCode', ]; } ``` @@ -251,6 +253,7 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static $searchable_fields = [ 'Name', 'BirthDate' => [ @@ -283,6 +286,7 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static string $general_search_field_name = 'my_general_field_name'; } ``` @@ -317,6 +321,7 @@ use SilverStripe\ORM\Filters\EndsWithFilter; class MyDataObject extends DataObject { + // ... private static string $general_search_field_filter = EndsWithFilter::class; } ``` @@ -350,6 +355,7 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static bool $general_search_split_terms = false; } ``` @@ -368,6 +374,7 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static $searchable_fields = [ 'Name', 'JobTitle', @@ -388,6 +395,7 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static $searchable_fields = [ 'Price', 'Description', @@ -405,6 +413,7 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static $general_search_field = 'Title'; } ``` @@ -433,15 +442,18 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static $searchable_fields = [ 'Name' => 'PartialMatchFilter', - 'ProductCode' => NumericField::class, + 'ProductCode' => [ + 'field' => NumericField::class, + ], ]; } ``` -If you assign a single string value, you can set it to be either a [FormField](api:SilverStripe\Forms\FormField) or [SearchFilter](api:SilverStripe\ORM\Filters\SearchFilter). To specify -both or to combine this with other configuration, you can assign an array: +If you assign a single string value, you can set it to be a [`SearchFilter`](api:SilverStripe\ORM\Filters\SearchFilter) class. To specify a specific [`FormField`](api:SilverStripe\Forms\FormField) to use or +specify both a form field *and* a filter - or to combine this with other configuration - you can assign an array: ```php namespace App\Model; @@ -452,6 +464,7 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static $searchable_fields = [ 'Name' => [ 'field' => TextField::class, @@ -466,6 +479,62 @@ class MyDataObject extends DataObject } ``` +#### Using `WithinRangeFilter` {#searchable-fields-withinrangefilter} + +If you want users to be able to filter by a field using a range, specify the [`WithinRangeFilter`](api:SilverStripe\ORM\Filters\WithinRangeFilter). This works out of the box with the numeric, date, datetime, and time fields that come in Silverstripe framework. + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; +SilverStripe\ORM\Filters\WithinRangeFilter + +class MyDataObject extends DataObject +{ + // ... + private static array $db = [ + 'Price' => 'Currency', + ]; + + private static array $searchable_fields = [ + 'Price' => [ + 'filter' => WithinRangeFilter::class, + ], + ]; +} +``` + +This configuration will duplicate the form field, providing one form field for the "from" value, and another for the "to" value. Users can then filter within a range using the filters in the CMS. + +![filter by price within a range](../../_images/withinrangefilter.png) + +If a user fills in only the "from" or "to" field, the other will be populated with the minimum or maximum value defined by the relevant `DBField` class in [`getMinValue()`](api:SilverStripe\ORM\FieldType\DBField::getMinValue()) or [`getMaxValue()`](api:SilverStripe\ORM\FieldType\DBField::getMaxValue()) + +This can also be used with other field types, but you need to define what the default "from" and "to" values should be. You can do this with the `rangeFromDefault` and `rangeToDefault` keys as shown below. + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; +SilverStripe\ORM\Filters\WithinRangeFilter + +class MyDataObject extends DataObject +{ + // ... + private static array $db = [ + 'Title' => 'Varchar', + ]; + + private static array $searchable_fields = [ + 'Title' => [ + 'filter' => WithinRangeFilter::class, + 'rangeFromDefault' => 'a', + 'rangeToDefault' => 'z', + ], + ]; +} +``` + ### Searching on relations To include relations (`$has_one`, `$has_many` and `$many_many`) in your search, you can use a dot-notation. @@ -477,6 +546,7 @@ use SilverStripe\ORM\DataObject; class Team extends DataObject { + // ... private static $db = [ 'Title' => 'Varchar', ]; @@ -499,6 +569,7 @@ use SilverStripe\ORM\DataObject; class Player extends DataObject { + // ... private static $db = [ 'Name' => 'Varchar', 'Birthday' => 'Date', @@ -515,7 +586,7 @@ class Player extends DataObject Use a single search field that matches on multiple database fields with `'match_any'`. This also supports specifying a `FormField` and a filter, though it is not necessary to do so. > [!CAUTION] -> If you don't specify a `FormField`, you must use the name of a real database field as the array key instead of a custom name so that a default field class can be determined. +> If you don't specify `field` or `dataType`, you must use the name of a real database field as the array key instead of a custom name so that a default field class can be determined. ```php namespace App\Model; @@ -524,6 +595,7 @@ use SilverStripe\Forms\TextField; class Order extends DataObject { + // ... private static $db = [ 'Name' => 'Varchar', ]; @@ -537,6 +609,8 @@ class Order extends DataObject 'CustomName' => [ 'title' => 'First Name', 'field' => TextField::class, + // Instead of defining "field" above, you could set "dataType" to a DBField instance like so: + // 'dataType' => DBVarchar::class, 'match_any' => [ // Searching with the "First Name" field will show Orders matching either // Name, Customer.FirstName, or ShippingAddress.FirstName @@ -549,6 +623,8 @@ class Order extends DataObject } ``` +You can also allow users to filter `match_any` with a range, using the configuration specified in [using `WithinRangeFilter`](#searchable-fields-withinrangefilter). + ## Summary fields Summary fields can be used to show a quick overview of the data for a specific [DataObject](api:SilverStripe\ORM\DataObject) record. The most common use @@ -561,6 +637,7 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static $db = [ 'Name' => 'Text', 'OtherProperty' => 'Text', @@ -585,6 +662,7 @@ use SilverStripe\ORM\DataObject; class OtherObject extends DataObject { + // ... private static $db = [ 'Title' => 'Varchar', ]; @@ -598,6 +676,7 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static $db = [ 'Name' => 'Text', 'Description' => 'HTMLText', @@ -626,6 +705,7 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static $db = [ 'Name' => 'Text', ]; @@ -652,6 +732,7 @@ use SilverStripe\ORM\DataObject; class MyDataObject extends DataObject { + // ... private static $db = [ 'Name' => 'Text', ]; diff --git a/en/02_Developer_Guides/09_Security/04_Sudo_Mode.md b/en/02_Developer_Guides/09_Security/04_Sudo_Mode.md index 4d50bd26a..db508cabc 100644 --- a/en/02_Developer_Guides/09_Security/04_Sudo_Mode.md +++ b/en/02_Developer_Guides/09_Security/04_Sudo_Mode.md @@ -6,11 +6,11 @@ icon: key # Sudo mode -Sudo mode represents a heightened level of permission in that you are more certain that the current user is actually the person whose account is logged in. This is performed by re-validating that the account's password is correct, and will then last for a certain amount of time (configurable) until it will be checked again. +Sudo mode represents a heightened level of permission in that you are more certain that the current CMS user is actually the person whose account is logged in. This is performed by re-validating that the account's password is correct, and will then last for a certain amount of time (configurable) until it will be checked again. -Sudo mode will automatically be enabled for the configured lifetime when a user logs into the CMS. Note that if the PHP session lifetime expires before the sudo mode lifetime, that sudo mode will also be cleared (and re-enabled when the user logs in again). If the user leaves their CMS open, or continues to use it, for an extended period of time with automatic refreshing in the background, sudo mode will eventually deactivate once the max lifetime is reached. +Sudo mode is not active when a user logs in to the CMS. If the PHP session lifetime expires before the sudo mode lifetime, that sudo mode will also be cleared. If the user leaves their CMS open, or continues to use it, for an extended period of time with automatic refreshing in the background, sudo mode will eventually deactivate once the max lifetime is reached. -## Configuring the lifetime +## Configuring the sudo mode lifetime The default [`SudoModeServiceInterface`](api:SilverStripe\Security\SudoMode\SudoModeServiceInterface) implementation is [`SudoModeService`](api:SilverStripe\Security\SudoMode\SudoModeService), and its lifetime can be configured with YAML. You should read the lifetime value using `SudoModeServiceInterface::getLifetime()`. @@ -19,39 +19,61 @@ SilverStripe\Security\SudoMode\SudoModeService: lifetime_minutes: 25 ``` -## Enabling sudo mode for controllers +## Sudo mode for models -You can add the `SudoModeServiceInterface` singleton as a dependency to a controller that requires sudo mode for one of its actions: +Models which are protected by sudo mode (usually a [`DataObject`](api:SilverStripe\ORM\DataObject) subclass) are still viewable without entering the password. Any actions that modify data, e.g. POSTing an edit form submission, will require the user to enter their password. This is done to balance usability with security. -```php -namespace App\Control; +The following `DataObject` subclasses are protected by sudo mode out of the box: -class MyController extends Controller -{ - private ?SudoModeServiceInterface $sudoModeService = null; +- [`Member`](api:SilverStripe\Security\Member) +- [`Group`](api:SilverStripe\Security\Group) +- [`PermissionRole`](api:SilverStripe\Security\PermissionRole) +- [`PermissionRoleCode`](api:SilverStripe\Security\PermissionRoleCode) - private static array $dependencies = ['SudoModeService' => '%$' . SudoModeServiceInterface::class]; +### Configuring sudo mode for your models - public function setSudoModeService(SudoModeServiceInterface $sudoModeService): static - { - $this->sudoModeService = $sudoModeService; - return $this; - } +To add sudo mode for a particular model, including your `DataObject` subclasses, simply set the [`require_sudo_mode`](api:SilverStripe\View\ViewableData->require_sudo_mode) configuration property to `true`, either directly on the class or via yml. + +> [!NOTE] +> This will only add sudo mode to edit forms within the CMS interface. It will have no effect on forms outside of the CMS, such as custom forms in the frontend. + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; + +class Product extends DataObject +{ + // ... + private static bool $require_sudo_mode = true; } ``` -Performing a sudo mode verification check in a controller action is simply using the service to validate the request: +```yml +SomeModule\Model\Player: + require_sudo_mode: true +``` + +> [!WARNING] If you add new fields to form that is protected by sudo mode, such as in an overridden [`LeftAndMain::getEditForm()`](api:SilverStripe\Admin\LeftAndMain::getEditForm()) method, it may mean having to call [`Form::requireSudoMode()`](api:SilverStripe\Forms\Form::requireSudoMode()) on the form to ensure the newly added fields are set to read-only while sudo mode is inactive. + +## Sudo mode for controller endpoints + +Performing a sudo mode verification check in a controller endpoint by using the sudo mode service to validate the request: ```php namespace App\Control; +use SilverStripe\Core\Injector\Injector; +use SilverStripe\Security\SudoMode\SudoModeServiceInterface; + class MyController extends Controller { // ... public function myAction(HTTPRequest $request): HTTPResponse { - if (!$this->sudoModeService->check($request->getSession())) { + $service = Injector::inst()->get(SudoModeServiceInterface::class); + if (!$service->check($request->getSession())) { return $this->httpError(403, 'Sudo mode is required for this action'); } // ... continue with sensitive operations @@ -59,14 +81,20 @@ class MyController extends Controller } ``` -## Using sudo mode in a react component +## Sudo mode for react components The `silverstripe/admin` module defines a [React Higher-Order-Component](https://reactjs.org/docs/higher-order-components.html) (aka HOC) which can be applied to React components in your module or code to intercept component rendering and show a "sudo mode required" information and log in screen, which will validate, activate sudo mode, and re-render the wrapped component afterwards on success. +This sudo mode differs from sudo mode for `DataObject`s in that it is not tied to a specific model, but rather to a +specific react component. + > [!WARNING] +> Sudo mode for react components does not protect the data the component manages, or any endpoints the component uses, it simply requires the user to re-enter their password before the component is rendered. + +> [!IMPORTANT] > The `WithSudoMode` HOC is exposed via [Webpack's expose-loader plugin](https://webpack.js.org/loaders/expose-loader/). You will need to add it as a [webpack external](https://webpack.js.org/configuration/externals/) to use it. The recommended way to do this is via the [@silverstripe/webpack-config npm package](https://www.npmjs.com/package/@silverstripe/webpack-config) which handles all the external configuration for you. You can get the injector to apply the HOC to your component automatically using [injector transformations](/developer_guides/customising_the_admin_interface/reactjs_and_redux/#transforming-services-using-middleware): diff --git a/en/08_Changelogs/6.0.0.md b/en/08_Changelogs/6.0.0.md index a2141fe7b..d3879b355 100644 --- a/en/08_Changelogs/6.0.0.md +++ b/en/08_Changelogs/6.0.0.md @@ -20,6 +20,7 @@ title: 6.0.0 (unreleased) - [Changes to default cache adapters](#caching) - [Changes to scaffolded form fields](#scaffolded-fields) - [`SiteTree` uses form field scaffolding](#sitetree-scaffolding) + - [Filter within a range with `searchable_fields`](#searchable-withinrange) - [Changes to the templating/view layer](#view-layer) - [Changes to `LeftAndMain` and its subclasses](#leftandmain-refactor) - [Changes to password validation](#password-validation) @@ -284,7 +285,6 @@ For example, you used to navigate to `/dev/tasks/` or use `sake dev/ta |[`ContentReviewEmails`](api:SilverStripe\ContentReview\Tasks\ContentReviewEmails)|`SilverStripe-ContentReview-Tasks-ContentReviewEmails`|`content-review-emails`| |[`DeleteAllJobsTask`](api:Symbiote\QueuedJobs\Tasks\DeleteAllJobsTask)|`Symbiote-QueuedJobs-Tasks-DeleteAllJobsTask`|`delete-queued-jobs`| |[`MigrateContentToElement`](api:DNADesign\Elemental\Tasks\MigrateContentToElement)|`DNADesign-Elemental-Tasks-MigrateContentToElement`|`elemental-migrate-content`| -|[`UserFormsColumnCleanTask`](api:SilverStripe\UserForms\Task\UserFormsColumnCleanTask)|`SilverStripe-UserForms-Task-UserFormsColumnCleanTask`|`userforms-column-clean`| |[`StaticCacheFullBuildTask`](api:SilverStripe\StaticPublishQueue\Task\StaticCacheFullBuildTask)|`SilverStripe-StaticPublishQueue-Task-StaticCacheFullBuildTask`|`static-cache-full-build`| #### Changes to `/dev/*` actions {#cli-dev} @@ -599,6 +599,40 @@ SilverStripe\UserForms\Model\UserDefinedForm: +### Filter within a range with `searchable_fields` {#searchable-withinrange} + +Using the `searchable_fields` configuration, you can now declare that a field should be filtered using a range. This works out of the box with the numeric, date, datetime, and time fields that come in the Silverstripe framework. + +> [!TIP] +> You can also provide ranges for other field types, but it requires some additional configuration. + +```php +namespace App\Model; + +use SilverStripe\ORM\DataObject; +use SilverStripe\ORM\Filters\WithinRangeFilter; + +class MyDataObject extends DataObject +{ + // ... + private static array $db = [ + 'Price' => 'Currency', + ]; + + private static array $searchable_fields = [ + 'Price' => WithinRangeFilter::class, + ]; +} +``` + +This configuration will duplicate the form field, providing one form field for the "from" value, and another for the "to" value. Users can then filter within a range using the filters in the CMS. + +![filter by price within a range](../_images/withinrangefilter.png) + +If a user fills in only the "from" or "to" field, the other will be populated with the minimum or maximum value defined by the relevant `DBField` class in [`getMinValue()`](api:SilverStripe\ORM\FieldType\DBField::getMinValue()) or [`getMaxValue()`](api:SilverStripe\ORM\FieldType\DBField::getMaxValue()) + +See the [searchable fields documentation](/developer_guides/model/scaffolding/#searchable-fields-withinrangefilter) for more details. + ### Changes to the templating/view layer {#view-layer} Note that the `SilverStripe\View\ViewableData` class has been renamed to [`SilverStripe\Model\ModelData`](api:SilverStripe\Model\ModelData). We will refer to it as `ModelData` in the rest of these change logs. diff --git a/en/_images/changelogs/5.2.0/elemental-badge.png b/en/_images/changelogs/5.2.0/elemental-badge.png deleted file mode 100644 index 9a2480ee3..000000000 Binary files a/en/_images/changelogs/5.2.0/elemental-badge.png and /dev/null differ diff --git a/en/_images/withinrangefilter.png b/en/_images/withinrangefilter.png new file mode 100644 index 000000000..ae9fc2f6e Binary files /dev/null and b/en/_images/withinrangefilter.png differ