From ca51cdf7081a6ee7c02e1fea661b69cdba1cd416 Mon Sep 17 00:00:00 2001 From: Guy Sartorelli <36352093+GuySartorelli@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:56:13 +1300 Subject: [PATCH] DOC Document new AdminController (#598) --- .../02_Controllers/07_CMS_JSON_APIs.md | 46 +++++++++---------- .../09_Security/01_Access_Control.md | 15 +++--- .../09_Security/02_Permissions.md | 2 +- .../02_CMS_Architecture.md | 24 +++++----- .../How_Tos/Customise_CMS_Pages_List.md | 2 +- .../How_Tos/Extend_CMS_Interface.md | 2 +- en/08_Changelogs/6.0.0.md | 16 ++++++- .../01_PHP_Coding_Conventions.md | 2 +- 8 files changed, 59 insertions(+), 50 deletions(-) diff --git a/en/02_Developer_Guides/02_Controllers/07_CMS_JSON_APIs.md b/en/02_Developer_Guides/02_Controllers/07_CMS_JSON_APIs.md index db91cbff2..de3880d00 100644 --- a/en/02_Developer_Guides/02_Controllers/07_CMS_JSON_APIs.md +++ b/en/02_Developer_Guides/02_Controllers/07_CMS_JSON_APIs.md @@ -21,34 +21,27 @@ Because of this you should generally avoid updating large parts of a DataObject ## Creating a controller -Create a subclass of [`LeftAndMain`](api:SilverStripe\Admin\LeftAndMain). This ensures that users must be logged in to the admin interface to access the endpoint. Additionally, it provides access to the methods [`LeftAndMain::jsonSuccess()`](api:SilverStripe\Admin\LeftAndMain::jsonSuccess()) and [`LeftAndMain::jsonError()`](api:SilverStripe\Admin\LeftAndMain::jsonError()). +Create a subclass of [`AdminController`](api:SilverStripe\Admin\AdminController). This ensures that users must be logged in to the admin interface to access the endpoint. Additionally, it provides access to the methods [`jsonSuccess()`](api:SilverStripe\Admin\AdminController::jsonSuccess()) and [`jsonError()`](api:SilverStripe\Admin\AdminController::jsonError()). > [!WARNING] -> To enhance security, do not create a direct subclass of [`Controller`](api:SilverStripe\Control\Controller) routed using YAML on the `/admin` route. This practice is strongly discouraged as it circumvents the requirement to log in to the CMS to access the endpoints. At best you'd be re-implementing logic that already exists. +> To enhance security, do not create a direct subclass of [`Controller`](api:SilverStripe\Control\Controller) routed using YAML on the `/admin/*` route. This practice is strongly discouraged as it circumvents the requirement to log in to the CMS to access the endpoints. At best you'd be re-implementing logic that already exists. When naming this class, it's best practice to add a "Controller" suffix to this class, for instance name it `MySomethingController`. -Define the URL segment of your controller using the [`url_segment`](api:SilverStripe\Admin\LeftAndMain->url_segment) configuration property. For example `private static string $url_segment = 'my-segment';`. For small optional modules, this may typically be the composer name of the module, for instance "linkfield". +Define the URL segment of your controller using the [`url_segment`](api:SilverStripe\Admin\AdminController->url_segment) configuration property. For example `private static string $url_segment = 'my-segment';`. For small optional modules, this may typically be the composer name of the module, for instance "linkfield". -Use the [`required_permission_codes`](api:SilverStripe\Admin\LeftAndMain->required_permission_codes) configuration property to declare what permissions are required to access endpoints on the controller. For example `private static string $required_permission_codes = 'CMS_ACCESS_CMSMain';`. +Use the [`required_permission_codes`](api:SilverStripe\Admin\AdminController->required_permission_codes) configuration property to declare what permissions are required to access endpoints on the controller. For example `private static string $required_permission_codes = 'CMS_ACCESS_CMSMain';`. See [user permissions](/developer_guides/security/permissions/) for more information about declaring permissions. -As this is a subclass of `LeftAndMain`, it automatically gets added to the CMS menu. To remove it from the CMS menu, create a `_config.php` in the module (if it doesn't already exist) and remove the controller from the menu like so: - -```php -use App\Controllers\MySomethingController; -use SilverStripe\Admin\CMSMenu; - -CMSMenu::remove_menu_class(MySomethingController::class); -``` +If you need form schema functionality, you will need to create a subclass of [`LeftAndMain`](api:SilverStripe\Admin\LeftAndMain) instead. All of the above still applies, but by default a menu item will be created for your new controller. To remove it from the CMS menu, set the [`ignore_menuitem`](api:SilverStripe\Admin\LeftAndMain->ignore_menuitem) configuration property to true for your class, i.e `private static $ignore_menuitem = true;`. ## Handling requests with `$url_handlers` Utilise the [`url_handlers`](api:SilverStripe\Control\Controller->url_handlers) configuration property to get the following benefits: - Ensure the HTTP request method aligns with the intended use for each method, for instance, restricting it to GET or POST. -- Prevent potential conflicts with existing methods, such as [`LeftAndMain::sort()`](api:SilverStripe\Admin\LeftAndMain::sort()), by structuring the endpoint URL segment as `sort` and associating it with a method like `MySomethingController::apiSort()`. +- If subclassing `LeftAndMain`, avoid potential conflicts with existing methods on the superclass, such as [`LeftAndMain::sort()`](api:SilverStripe\Admin\LeftAndMain::sort()), by structuring the endpoint URL segment as `sort` and associating it with a method like `MySomethingController::apiSort()`. Use the request param `$ItemID` if you need a record ID into a URL so that you have an endpoint for a specific record. Use `$ItemID` because it's consistent with the request param used in Form Schema requests. For example, to use `$ItemID` in a GET request to view a single record: @@ -56,10 +49,10 @@ Use the request param `$ItemID` if you need a record ID into a URL so that you h // app/src/Controllers/MySomethingController.php namespace App\Controllers; -use SilverStripe\Admin\LeftAndMain; +use SilverStripe\Admin\AdminController; use SilverStripe\Control\HTTPResponse; -class MySomethingController extends LeftAndMain +class MySomethingController extends AdminController { // ... private static array $url_handlers = [ @@ -87,7 +80,9 @@ See [URL handlers](/developer_guides/controllers/routing/#url-handlers) for more ## Permission checks -Incorporate essential permission checks, such as `canEdit()`, into all relevant endpoints to ensure secure access control. +As mentioned in [creating a controller](#creating-a-controller) above, any permissions you add to the `required_permission_codes` configuration property for your controller will be checked before initialising the controller. + +You should also incorporate additional permission checks, such as calling `canEdit()` on a `DataObject` record, into all relevant endpoints to ensure secure access control. When returning `DataObject` records as JSON, remember to invoke `canView()` on each record. In a CMS context where the number of records is typically limited (e.g. by pagination), the performance impact of these checks should not be a significant concern. If the permission check fails then call `$this->jsonError(403);` to return a 403 status code. @@ -146,7 +141,7 @@ if (!SecurityToken::inst()->checkRequest($this->getRequest())) { ## Passing values from PHP to global JavaScript -To transmit values from PHP to global JavaScript, which is used for component configuration as opposed to data, override `LeftAndMain::getClientConfig()` within your controller. Begin your method with `$clientConfig = parent::getClientConfig();` to ensure proper inheritance. +To transmit values from PHP to global JavaScript, which is used for component configuration as opposed to data, override [`getClientConfig()`](api:SilverStripe\Admin\AdminController::getClientConfig()) within your controller. Begin your method with `$clientConfig = parent::getClientConfig();` to ensure proper inheritance, or better yet, use [`beforeExtending()`](api:SilverStripe\Core\Extensible::beforeExtending()) so that extensions implementing the `updateClientConfig()` extension hook can update your config. Include any relevant links to endpoints in the client configuration. For example, add `'myEndpointUrl' => $this->Link('my-endpoint')`, where `my-endpoint` is specified in `private static array $url_handlers`. @@ -154,22 +149,23 @@ Include any relevant links to endpoints in the client configuration. For example // app/src/Controllers/MySomethingController.php namespace App\Controllers; -use SilverStripe\Admin\LeftAndMain; +use SilverStripe\Admin\AdminController; -class MySomethingController extends LeftAndMain +class MySomethingController extends AdminController { // ... private static array $url_handlers = [ 'my-endpoint' => 'apiEndpoint', ]; - public function getClientConfig() + public function getClientConfig(): array { - $clientConfig = parent::getClientConfig(); - $clientConfig['myForm'] = [ - 'myEndpointUrl' => $this->Link('my-endpoint'), - ]; - return $clientConfig; + $this->beforeExtending('updateClientConfig', function (array &$clientConfig): void { + $clientConfig['myForm'] = [ + 'myEndpointUrl' => $this->Link('my-endpoint'), + ]; + }); + return parent::getClientConfig(); } } ``` diff --git a/en/02_Developer_Guides/09_Security/01_Access_Control.md b/en/02_Developer_Guides/09_Security/01_Access_Control.md index 4ef425c98..46cc84f5d 100644 --- a/en/02_Developer_Guides/09_Security/01_Access_Control.md +++ b/en/02_Developer_Guides/09_Security/01_Access_Control.md @@ -41,10 +41,9 @@ privileges from its parent group. ## Permission checking is at class level -Silverstripe CMS provides a security mechanism via the *Permission::check* method (see [LeftAndMain](api:SilverStripe\Admin\LeftAndMain) for examples on how -the admin screens work). +Silverstripe CMS provides a security mechanism via the `Permission::check()` method (see [`AdminController::init()`](api:SilverStripe\Admin\AdminController::init()) for an example of how permission checks can be used). -(next step -- go from *Permission::checkMember*...) +(next step -- go from `Permission::checkMember()`...) ### Nuts and bolts -- figuring it out @@ -53,13 +52,13 @@ works. ### Loading the admin page: looking at security -If you go to [your site]/admin `Director.php` maps the 'admin' URL request through a [Director](api:SilverStripe\Control\Director) rule to the -[CMSMain](api:SilverStripe\CMS\Controllers\CMSMain) controller (see [CMSMain](api:SilverStripe\CMS\Controllers\CMSMain), with no arguments). +If you go to [your site]/admin `Director.php` maps the 'admin' URL request through a [`Director`](api:SilverStripe\Control\Director) rule to the +[`CMSMain`](api:SilverStripe\CMS\Controllers\CMSMain) controller (see [`CMSMain`](api:SilverStripe\CMS\Controllers\CMSMain), with no arguments). -*CMSMain.init()* calls its parent which, of all things is called [LeftAndMain](api:SilverStripe\Admin\LeftAndMain). It's in [LeftAndMain](api:SilverStripe\Admin\LeftAndMain) that the -important security checks are made by calling *Permission::check*. +`CMSMain::init()` calls its parent which, of all things is called [`AdminController`](api:SilverStripe\Admin\AdminController). It's in `AdminController` that the +important security checks are made by calling `Permission::check()`. -[Security::permissionFailure()](api:SilverStripe\Security\Security::permissionFailure()) is the next utility function you can use to redirect to the login form. +[`Security::permissionFailure()`](api:SilverStripe\Security\Security::permissionFailure()) is the next utility function you can use to redirect to the login form. ### Customizing access checks in CMS classes diff --git a/en/02_Developer_Guides/09_Security/02_Permissions.md b/en/02_Developer_Guides/09_Security/02_Permissions.md index e53e0224f..361d766ec 100644 --- a/en/02_Developer_Guides/09_Security/02_Permissions.md +++ b/en/02_Developer_Guides/09_Security/02_Permissions.md @@ -91,7 +91,7 @@ Access to the CMS has a couple of special cases where permission codes can imply #### 1. Granting access to all CMS permissions -The `CMS_ACCESS_LeftAndMain` grants access to every single area of the CMS, without exception. Internally, this works by +The `CMS_ACCESS_LeftAndMain` permission grants access to every single area of the CMS, without exception. Internally, this works by adding the `CMS_ACCESS_LeftAndMain` code to the set of accepted codes when a `CMS_ACCESS_*` permission is required. This works much like ADMIN permissions (see above) diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md index 1f821b305..9b3c4176c 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/02_CMS_Architecture.md @@ -57,7 +57,7 @@ The Silverstripe CMS pattern library is built using the [StoryBook JS library](h ## The admin URL -The CMS interface can be accessed by default through the `admin/` URL. You can change this by setting the `$url_base` config for the [AdminRootController](api:SilverStripe\Admin\AdminRootController), creating your own [Director](api:SilverStripe\Control\Director) routing rule and clearing the old rule as per the example below: +The CMS interface can be accessed by default through the `/admin` URL. You can change this by setting the `$url_base` configuration property for the [`AdminRootController`](api:SilverStripe\Admin\AdminRootController), creating your own [`Director`](api:SilverStripe\Control\Director) routing rule and clearing the old rule as per the example below: ```yml --- @@ -117,18 +117,20 @@ joinUrlPaths(ss.config.adminUrl, 'more/path/here'); ### Multiple admin URL and overrides -You can also create your own classes that extend the [AdminRootController](api:SilverStripe\Admin\AdminRootController) to create multiple or custom admin areas, with a `Director.rules` for each one. +You can also create your own classes that extend the [`AdminRootController`](api:SilverStripe\Admin\AdminRootController) to create multiple or custom admin areas, with a `Director.rules` for each one. ## Templates and controllers -The CMS backend is handled through the [LeftAndMain](api:SilverStripe\Admin\LeftAndMain) controller class, +The base controller for the CMS is [`AdminController`](api:SilverStripe\Admin\AdminController). This is a simple controller that checks users have the relevant permissions before initialising. `AdminRootController` provides an appropriate routing rule for all non-abstract subclasses of `AdminController`. + +The CMS backend UI is primarily handled through the [`LeftAndMain`](api:SilverStripe\Admin\LeftAndMain) controller class, which contains base functionality like displaying and saving a record. -This is extended through various subclasses, e.g. to add a group hierarchy ([SecurityAdmin](api:SilverStripe\Admin\SecurityAdmin)), -a search interface ([ModelAdmin](api:SilverStripe\Admin\ModelAdmin)) or an "Add Page" form ([CMSPageAddController](api:SilverStripe\CMS\Controllers\CMSPageAddController)). +This is extended through various subclasses, e.g. to add a group hierarchy ([`SecurityAdmin`](api:SilverStripe\Admin\SecurityAdmin)), +a search interface ([`ModelAdmin`](api:SilverStripe\Admin\ModelAdmin)) or an "Add Page" form ([`CMSPageAddController`](api:SilverStripe\CMS\Controllers\CMSPageAddController)). The controller structure is too complex to document here, a good starting point -for following the execution path in code are [LeftAndMain::getRecord()](api:SilverStripe\Admin\LeftAndMain::getRecord()) and [LeftAndMain::getEditForm()](api:SilverStripe\Admin\LeftAndMain::getEditForm()). -If you have the `cms` module installed, have a look at [CMSMain::getEditForm()](api:SilverStripe\CMS\Controllers\CMSMain::getEditForm()) for a good +for following the execution path in code are [`LeftAndMain::getRecord()`](api:SilverStripe\Admin\LeftAndMain::getRecord()) and [`LeftAndMain::getEditForm()`](api:SilverStripe\Admin\LeftAndMain::getEditForm()). +If you have the `cms` module installed, have a look at [`CMSMain::getEditForm()`](api:SilverStripe\CMS\Controllers\CMSMain::getEditForm()) for a good example on how to extend the base functionality (e.g. by adding page versioning hints to the form). CMS templates are inherited based on their controllers, similar to subclasses of @@ -142,13 +144,13 @@ which is in charge of rendering the main content area apart from the CMS menu. Depending on the complexity of your layout, you'll also need to override the "EditForm" template (e.g. `MyCMSController_EditForm.ss`), e.g. to implement a tabbed form which only scrolls the main tab areas, while keeping the buttons at the bottom of the frame. -This requires manual assignment of the template to your form instance, see [CMSMain::getEditForm()](api:SilverStripe\CMS\Controllers\CMSMain::getEditForm()) for details. +This requires manual assignment of the template to your form instance, see [`CMSMain::getEditForm()`](api:SilverStripe\CMS\Controllers\CMSMain::getEditForm()) for details. Often its useful to have a "tools" panel in between the menu and your content, usually occupied by a search form or navigational helper. In this case, you can either override the full base template as described above. -To avoid duplicating all this template code, you can also use the special [LeftAndMain::Tools()](api:SilverStripe\Admin\LeftAndMain::Tools()) and -[LeftAndMain::EditFormTools()](api:SilverStripe\Admin\LeftAndMain::EditFormTools()) methods available in `LeftAndMain`. +To avoid duplicating all this template code, you can also use the special [`LeftAndMain::Tools()`](api:SilverStripe\Admin\LeftAndMain::Tools()) and +[`LeftAndMain::EditFormTools()`](api:SilverStripe\Admin\LeftAndMain::EditFormTools()) methods available in `LeftAndMain`. These placeholders are populated by auto-detected templates, with the naming convention of `_Tools.ss` and `_EditFormTools.ss`. So to add or "subclass" a tools panel, simply create this file and it's automatically picked up. @@ -341,7 +343,7 @@ class MyAdmin extends LeftAndMain { // ... - public function getClientConfig() + public function getClientConfig(): array { return array_merge(parent::getClientConfig(), [ 'reactRouter' => true, diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Customise_CMS_Pages_List.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Customise_CMS_Pages_List.md index 81d590d84..e0bca27bf 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Customise_CMS_Pages_List.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Customise_CMS_Pages_List.md @@ -49,7 +49,7 @@ class NewsPage extends Page } ``` -We'll now add an `Extension` subclass to `LeftAndMain`, which is the main CMS controller. +We'll now add an `Extension` subclass to `LeftAndMain`, which is the main CMS UI controller. This allows us to intercept the list building logic, and alter the `GridField` before its rendered. In this case, we limit our logic to the desired page type, although it's just as easy to implement changes which apply to all page types, diff --git a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md index 8126848b1..4b716595b 100644 --- a/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md +++ b/en/02_Developer_Guides/15_Customising_the_Admin_Interface/How_Tos/Extend_CMS_Interface.md @@ -145,7 +145,7 @@ Refresh the CMS, open a page for editing and you should see the new checkbox. One piece in the puzzle is still missing: How do we get the list of bookmarked pages from the database into the template we've already created (with hardcoded -links)? Again, we extend a core class: The main CMS controller called +links)? Again, we extend a core class: The main CMS UI controller called `LeftAndMain`. Add the following code to a new file `app/src/BookmarkedLeftAndMainExtension.php`; diff --git a/en/08_Changelogs/6.0.0.md b/en/08_Changelogs/6.0.0.md index 6860660bb..ecf7513ae 100644 --- a/en/08_Changelogs/6.0.0.md +++ b/en/08_Changelogs/6.0.0.md @@ -14,6 +14,7 @@ title: 6.0.0 (unreleased) - [Changes to scaffolded form fields](#scaffolded-fields) - [`SiteTree` uses form field scaffolding](#sitetree-scaffolding) - [Changes to the templating/view layer](#view-layer) + - [Changes to `LeftAndMain` and its subclasses](#leftandmain-refactor) - [Changes to password validation](#password-validation) - [Other new features](#other-new-features) - [Dependency changes](#dependency-changes) @@ -471,6 +472,17 @@ The one change we specifically want to call out is for [`ModelData::obj()`](api: See the [full list of removed and changed API](#api-removed-and-changed) to see all of the API with updated typing. +### Changes to `LeftAndMain` and its subclasses {#leftandmain-refactor} + +[`LeftAndMain`](api:SilverStripe\Admin\LeftAndMain) has historically been the superclass for all controllers routed in `/admin/*` (i.e. all controllers used in the CMS). That class includes a lot of boilerplate functionality for setting up a menu, edit form, etc which a lot of controllers don't need. + +A new [`AdminController`](api:SilverStripe\Admin\AdminController) has been created which provides the `/admin/*` routing functionality and permission checks that `LeftAndMain` used to be responsible for. If you have a controller which needs to be routed through `/admin/*` with the relevant CMS permission checks, but which does *not* need a menu item on the left or an edit form, you should update that class to be a subclass of `AdminController` instead. + +As a result of this change, to following classes are now direct subclasses of `AdminController`: + +- [`SudoModeController`](api:SilverStripe\Admin\SudoModeController) - used to be a subclass of `LeftAndMain`. +- [`CMSExternalLinksController`](api:SilverStripe\ExternalLinks\Controllers\CMSExternalLinksController) - used to be a direct subclass of [`Controller`](api:SilverStripe\Control\Controller). + ### Changes to password validation {#password-validation} #### `PasswordValidator` changes @@ -486,7 +498,7 @@ SilverStripe\Security\Validation\EntropyPasswordValidator: password_strength: 4 ``` -`EntropyPasswordValidator` also has the same options for avoiding repeate uses of the same password that `RulesPasswordValidator` has. +`EntropyPasswordValidator` also has the same options for avoiding repeat uses of the same password that `RulesPasswordValidator` has. This does not retroactively affect existing passwords, but will affect any new passwords (e.g. new members or changing the password of an existing member). @@ -509,7 +521,7 @@ If [ConfirmedPasswordField->requireStrongPassword](api:SilverStripe\Forms\Confir This has been changed to use the [`PasswordStrength` constraint](https://symfony.com/doc/current/reference/constraints/PasswordStrength.html) in `symfony/validator` instead. Now a password is considered "strong" based on its level of entropy. -You can change the level of entropy required by passing one of the valid [minScore values](https://symfony.com/doc/current/reference/constraints/PasswordStrength.html#minscore) into [`api:SilverStripe\Forms\ConfirmedPasswordField::setMinPasswordStrength()`](ConfirmedPasswordField::setMinPasswordStrength()). +You can change the level of entropy required by passing one of the valid [minScore values](https://symfony.com/doc/current/reference/constraints/PasswordStrength.html#minscore) into [`ConfirmedPasswordField::setMinPasswordStrength()`](api:SilverStripe\Forms\ConfirmedPasswordField::setMinPasswordStrength()). ### Other new features diff --git a/en/10_Contributing/05_Coding_Conventions/01_PHP_Coding_Conventions.md b/en/10_Contributing/05_Coding_Conventions/01_PHP_Coding_Conventions.md index 4be4d808b..546538688 100644 --- a/en/10_Contributing/05_Coding_Conventions/01_PHP_Coding_Conventions.md +++ b/en/10_Contributing/05_Coding_Conventions/01_PHP_Coding_Conventions.md @@ -35,7 +35,7 @@ These coding conventions are for new code, and for situations where you are inte Use an appropriate suffix or prefix for classnames when making a subclass or implementing an interface. Usually the suffix will be the name of the parent class or the interface. Sometimes the suffix/prefix is a shortened version of the name of the parent because it reads better while retaining easy comprehension. Here are some common examples with the parent class or interface in brackets: -- `Admin` ([`ModelAdmin`](api:SilverStripe\Admin\ModelAdmin), [`LeftAndMain`]((api:SilverStripe\Admin\LeftAndAdmin)) if included in the CMS Menu) +- `Admin` ([`ModelAdmin`](api:SilverStripe\Admin\ModelAdmin), [`LeftAndMain`](api:SilverStripe\Admin\LeftAndAdmin) if included in the CMS Menu) - `Block` ([`BaseElement`](api:DNADesign\Elemental\Models\BaseElement)) - `DB` ([`DBField`](api:SilverStripe\ORM\FieldType\DBField)) - use as a prefix, e.g. `DBString` - `Controller` ([`Controller`](api:SilverStripe\Control\Controller))