Skip to content

Document that readOnly is only a control in the relation configuration - #269

Merged
LukeTowers merged 3 commits into
1.2from
docs/relation-read-only
Oct 30, 2026
Merged

LukeTowers merged 3 commits into
1.2from
docs/relation-read-only

Conversation

@LukeTowers

Copy link
Copy Markdown
Member

relationRender('comments', ['readOnly' => true]) reads as a way to forbid writes, but the value goes to the browser and comes back with each relation AJAX request, so the client decides whether it returns at all. It can only ever make a relation more restricted than its configuration says. The configuration key, and relationExtendConfig(), are the forms that are enforced.

Also documents what a read only relation actually does now that the handlers refuse, and the preview-mode contract form widgets have to honour in their own AJAX handlers.

`relationRender('comments', ['readOnly' => true])` reads as a way to forbid writes, but the value goes to the browser and comes back with each relation AJAX request, so the client decides whether it returns at all. It can only ever make a relation more restricted than its configuration says. The configuration key, and `relationExtendConfig()`, are the forms that are enforced.

Also documents what a read only relation actually does now that the handlers refuse, and the preview-mode contract form widgets have to honour in their own AJAX handlers.
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 84ac0f77-26e0-4a9f-b46d-9ec214491eaf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

applyExtraConfig() keeps only recordUrl and recordOnClick, so the value is dropped in both directions rather than restricting the render further.
@LukeTowers
LukeTowers merged commit cb2a068 into 1.2 Oct 30, 2026
2 checks passed
@LukeTowers
LukeTowers deleted the docs/relation-read-only branch October 30, 2026 03:55
LukeTowers added a commit to wintercms/winter that referenced this pull request Oct 30, 2026
… write paths (#1554)

Three controller behaviours decide what a user may act on, then resolve the ids posted back without consulting that decision.

Relation. Posted ids (manage_id, record_id, foreign_id, checked[]) resolved against the related model rather than through the relation, so a user managing one parent's relation could reach another parent's records, and the handlers that attach an existing record had no server-side notion of the selectable set — ignoring the manage.conditions and manage.scope options that narrow it. Separately readOnly was consulted only while rendering, so every write handler still wrote, including the AJAX handlers of field widgets inside the relation's forms.

List. Lists::getSelectionQuery() refused a whole-query selection when the list renders no checkboxes but accepted explicit ids on the same list, so index_onDelete could be handed ids for a list offering no selection. In core that reaches the authentication audit trail: AccessLogs declares no showCheckboxes, renders only a Refresh button, and is gated by system.access_logs — "View system logs".

Reorder. onReorder() resolved record_ids and sourceNode/targetNode against the bare model, so reorderExtendQuery() narrowed the listing but not the write.

Each now resolves through the query it already uses to decide what to show. A readOnly passed to relationRender() is presentational and cannot be enforced — the client decides whether it returns — so only the relation's configuration is a control; documented in wintercms/docs#269.

Upgrade notes:

A read-only relation refuses every write handler with a 403, including drag-and-drop reordering and the FileUpload and Repeater handlers inside its forms. Rendering, paging and searching are unchanged.
readOnly counts only from the relation configuration or relationExtendConfig(); a value passed to relationRender() no longer does, and the behaviour's $readOnly property is always a boolean.
Posted record ids the relation manager does not offer are ignored, and the manage and pivot forms raise the usual "record not found". A record only the backend.list.extendRecords event injects is displayed but not manageable.
A list declaring no showCheckboxes refuses a selection however the ids arrive, so a controller driving index_onDelete from its own UI must declare it.
reorderExtendQuery() narrows what may be reordered as well as what is listed.
bennothommo pushed a commit to wintercms/wn-system-module that referenced this pull request Oct 30, 2026
… write paths (#1554)

Three controller behaviours decide what a user may act on, then resolve the ids posted back without consulting that decision.

Relation. Posted ids (manage_id, record_id, foreign_id, checked[]) resolved against the related model rather than through the relation, so a user managing one parent's relation could reach another parent's records, and the handlers that attach an existing record had no server-side notion of the selectable set — ignoring the manage.conditions and manage.scope options that narrow it. Separately readOnly was consulted only while rendering, so every write handler still wrote, including the AJAX handlers of field widgets inside the relation's forms.

List. Lists::getSelectionQuery() refused a whole-query selection when the list renders no checkboxes but accepted explicit ids on the same list, so index_onDelete could be handed ids for a list offering no selection. In core that reaches the authentication audit trail: AccessLogs declares no showCheckboxes, renders only a Refresh button, and is gated by system.access_logs — "View system logs".

Reorder. onReorder() resolved record_ids and sourceNode/targetNode against the bare model, so reorderExtendQuery() narrowed the listing but not the write.

Each now resolves through the query it already uses to decide what to show. A readOnly passed to relationRender() is presentational and cannot be enforced — the client decides whether it returns — so only the relation's configuration is a control; documented in wintercms/docs#269.

Upgrade notes:

A read-only relation refuses every write handler with a 403, including drag-and-drop reordering and the FileUpload and Repeater handlers inside its forms. Rendering, paging and searching are unchanged.
readOnly counts only from the relation configuration or relationExtendConfig(); a value passed to relationRender() no longer does, and the behaviour's $readOnly property is always a boolean.
Posted record ids the relation manager does not offer are ignored, and the manage and pivot forms raise the usual "record not found". A record only the backend.list.extendRecords event injects is displayed but not manageable.
A list declaring no showCheckboxes refuses a selection however the ids arrive, so a controller driving index_onDelete from its own UI must declare it.
reorderExtendQuery() narrows what may be reordered as well as what is listed.
bennothommo pushed a commit to wintercms/wn-backend-module that referenced this pull request Oct 30, 2026
… write paths (#1554)

Three controller behaviours decide what a user may act on, then resolve the ids posted back without consulting that decision.

Relation. Posted ids (manage_id, record_id, foreign_id, checked[]) resolved against the related model rather than through the relation, so a user managing one parent's relation could reach another parent's records, and the handlers that attach an existing record had no server-side notion of the selectable set — ignoring the manage.conditions and manage.scope options that narrow it. Separately readOnly was consulted only while rendering, so every write handler still wrote, including the AJAX handlers of field widgets inside the relation's forms.

List. Lists::getSelectionQuery() refused a whole-query selection when the list renders no checkboxes but accepted explicit ids on the same list, so index_onDelete could be handed ids for a list offering no selection. In core that reaches the authentication audit trail: AccessLogs declares no showCheckboxes, renders only a Refresh button, and is gated by system.access_logs — "View system logs".

Reorder. onReorder() resolved record_ids and sourceNode/targetNode against the bare model, so reorderExtendQuery() narrowed the listing but not the write.

Each now resolves through the query it already uses to decide what to show. A readOnly passed to relationRender() is presentational and cannot be enforced — the client decides whether it returns — so only the relation's configuration is a control; documented in wintercms/docs#269.

Upgrade notes:

A read-only relation refuses every write handler with a 403, including drag-and-drop reordering and the FileUpload and Repeater handlers inside its forms. Rendering, paging and searching are unchanged.
readOnly counts only from the relation configuration or relationExtendConfig(); a value passed to relationRender() no longer does, and the behaviour's $readOnly property is always a boolean.
Posted record ids the relation manager does not offer are ignored, and the manage and pivot forms raise the usual "record not found". A record only the backend.list.extendRecords event injects is displayed but not manageable.
A list declaring no showCheckboxes refuses a selection however the ids arrive, so a controller driving index_onDelete from its own UI must declare it.
reorderExtendQuery() narrows what may be reordered as well as what is listed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant