Skip to content

Commit

Permalink
DOC Document new AdminController (#598)
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli authored Oct 20, 2024
1 parent 2d4bbe0 commit ca51cdf
Show file tree
Hide file tree
Showing 8 changed files with 59 additions and 50 deletions.
46 changes: 21 additions & 25 deletions en/02_Developer_Guides/02_Controllers/07_CMS_JSON_APIs.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,45 +21,38 @@ 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:

```php
// 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 = [
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -146,30 +141,31 @@ 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`.

```php
// 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();
}
}
```
Expand Down
15 changes: 7 additions & 8 deletions en/02_Developer_Guides/09_Security/01_Access_Control.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion en/02_Developer_Guides/09_Security/02_Permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down Expand Up @@ -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
Expand All @@ -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 `<controller classname>_Tools.ss` and `<controller classname>_EditFormTools.ss`.
So to add or "subclass" a tools panel, simply create this file and it's automatically picked up.
Expand Down Expand Up @@ -341,7 +343,7 @@ class MyAdmin extends LeftAndMain
{
// ...

public function getClientConfig()
public function getClientConfig(): array
{
return array_merge(parent::getClientConfig(), [
'reactRouter' => true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down
Loading

0 comments on commit ca51cdf

Please sign in to comment.