Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -1294,7 +1294,7 @@ records:

Option | Description
------------- | -------------
`readOnly` | forces the relation controller in readOnly mode. (defaults to parent previewMode)
`readOnly` | forces the relation controller in readOnly mode. (defaults to parent previewMode). Presentational only - to forbid writes, set `readOnly` in the [relation configuration](relations#read-only-relations).
`recordUrl` | path to controller action to open a record (e.g. `author/plugin/controller/update/:id`). `:id` will get replaced with the record id.
`recordOnClick` | custom Javascript code to execute when a record is clicked.
`relation` | relation name if different from the field name.
Expand Down
18 changes: 17 additions & 1 deletion backend/relations.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Option | Description
`manage` | configuration specific to the management popup, see below.
`pivot` | a reference to form field definition file, used for [relations with pivot table data](#belongs-to-many-with-pivot-data).
`emptyMessage` | a message to display when the relationship is empty, optional.
`readOnly` | disables the ability to add, update, delete or create relations. default: `false`
`readOnly` | forbids adding, updating, deleting and creating, and is enforced server-side. See [Read only relations](#read-only-relations). default: `false`
`deferredBinding` | [defers all binding actions using a session key](../database/relations#deferred-binding) when it is available. default: `false`

These configuration values can be specified for the **view** or **manage** options, where applicable to the render type of list, form or both.
Expand All @@ -106,6 +106,8 @@ Option | Type | Description
`scope` | List | specifies a [query scope method](../database/model#query-scopes) defined in the **related form model** to apply to the list query always. The model that this relationship will be attached to (i.e. the **parent model**) is passed to this scope method as the second parameter (`$query` is the first).
**filter** | List | a reference to a filter scopes definition file, see [backend list filters](lists#using-list-filters).

> **NOTE:** `conditions`, `scope` and `relationExtendManageWidget()` narrow the manage list that **Add** and **Link** select from, and only a record appearing in it may be attached - so they restrict what may be attached, not just what is shown. Every other record the relation manager touches is resolved through the relation itself.

These configuration values can be specified only for the **view** options.

Option | Type | Description
Expand Down Expand Up @@ -332,6 +334,20 @@ You may instruct the relation manager to render in read only mode by passing the
<?= $this->relationRender('comments', ['readOnly' => true]) ?>
```

> **IMPORTANT**: this styles that one render and nothing else. It is not an authorization control and is not remembered: the relation's AJAX handlers read `readOnly` from the relation configuration alone, so passing it here neither forbids a write nor allows one.
>
> To forbid writes, set `readOnly` in the relation configuration, or per request from [`relationExtendConfig()`](#extending-relation-configuration). Both are enforced server-side.

## Read only relations

Every relation management handler - create, update, delete, add, remove and the pivot handlers - refuses with HTTP 403, drag-and-drop reordering is disabled, and the view, manage and pivot forms render in preview mode.

Only writing is forbidden. The relation still renders and its list can still be searched, filtered, sorted and paged.

Preview mode propagates to every field widget in those forms, so a widget's own write handlers refuse too - they are addressed by widget alias and would otherwise reach the relation without passing through its authorization.

> **NOTE**: a third-party widget that does not call `abortIfPreviewMode()` in its write handlers will still write inside a read only relation. See [preview mode and AJAX handlers](widgets#preview-mode-and-ajax-handlers) if you maintain one.

## Extending relation behavior

Sometimes you may wish to modify the default relation behavior and there are several ways you can do this.
Expand Down
21 changes: 21 additions & 0 deletions backend/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,27 @@ born_at:
maxDate: 2014-04-23
```

### Preview mode and AJAX handlers

A form widget carries a `previewMode` property that is `true` whenever its field is not editable. You do not set it yourself; it is raised by the parent form on every field widget it builds, by a `disabled` field, and by a [read only relation](relations#read-only-relations) on its forms.

Widgets already hide their controls in that state, but a handler is dispatched straight to the widget by alias - `mywidget::onDoSomething` - so it never passes through whatever authorized the page. **Any handler that writes must refuse:**

```php
public function onDeleteSomething()
{
$this->abortIfPreviewMode();

// ...
}
```

`abortIfPreviewMode()` aborts with a `403` when `previewMode` is set, and does nothing otherwise.

Handlers that only read must **not** call it - a preview form is still meant to be looked at, so refreshing a preview or loading a picker keeps working. Core widgets follow this: `FileUpload::onUpload` and `Repeater::onAddItem` are guarded, `FileUpload::onLoadAttachmentConfig` and `MarkdownEditor::onRefresh` are not.

> **NOTE**: this is a contract, not an enforcement. A widget that ignores `previewMode` stays writable on a preview form, just as it already renders its editable controls there.

### Form widget registration

Plugins should register form widgets by overriding the `registerFormWidgets` method inside the [Plugin registration class](../plugin/registration#registration-file). The method returns an array containing the widget class in the keys and widget short code as the value. Example:
Expand Down
Loading