diff --git a/backend/forms.md b/backend/forms.md index 178f6b07..2600150d 100644 --- a/backend/forms.md +++ b/backend/forms.md @@ -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. diff --git a/backend/relations.md b/backend/relations.md index 4c744831..16a57a8b 100644 --- a/backend/relations.md +++ b/backend/relations.md @@ -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. @@ -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 @@ -332,6 +334,20 @@ You may instruct the relation manager to render in read only mode by passing the 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. diff --git a/backend/widgets.md b/backend/widgets.md index 22749496..edbc761d 100644 --- a/backend/widgets.md +++ b/backend/widgets.md @@ -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: