fix(16.2): WR-01 check the declared save before a field upload

The field upload route ran only protect, so the create form of a
controller without the create toolbar button could still write files to
the public bucket. It now refuses with 403 where the save route would:
create for {id} 0, update otherwise (operationDeclared).
This commit is contained in:
2026-10-10 16:42:28 +02:00
parent 9899ac61dd
commit 3158bb5b57
5 changed files with 50 additions and 4 deletions

View File

@@ -118,7 +118,7 @@ A column in `columns.yaml` may name a plugin alias as its `type`. The list then
## Uploads
A type that stores files (images in a document, say) sets `Upload`. The element never uploads anything itself: it asks the admin, which posts the file with the administrator's session to `POST <prefix>/api/v1/{vendor}/{plugin}/{controller}/{id}/fields/{field}/upload` as one multipart part named `file`. cabana checks the backend session, the `X-Requested-With` header, the controller's permissions, that the field has a plugin type with `Upload` and that its `context` allows the operation, then caps the body at `http.body_limits.upload_bytes`. `{id}` 0 is the create form; any other id loads the record through the controller's `pact.FormExtendQuery` scope inside a transaction, and a record outside it is a 404. `Upload` receives a `pact.FormFieldUpload` with the transaction, the record (nil on the create form), the field, its options, the file name and the capped body, and returns the URL the element shows. The route answers 201 `{"data": {"url": "..."}}`; a `pact.FormFieldError` is a 422 on the field, a body over the cap a 413, and any other error the opaque 500. Checking the file (its size, its real type) is the type's job.
A type that stores files (images in a document, say) sets `Upload`. The element never uploads anything itself: it asks the admin, which posts the file with the administrator's session to `POST <prefix>/api/v1/{vendor}/{plugin}/{controller}/{id}/fields/{field}/upload` as one multipart part named `file`. cabana checks the backend session, the `X-Requested-With` header, the controller's permissions, that the controller declares the save the upload belongs to (create for `{id}` 0, which needs the list's `create` toolbar button, update otherwise; a 403 otherwise, as the save routes answer), that the field has a plugin type with `Upload` and that its `context` allows the operation, then caps the body at `http.body_limits.upload_bytes`. `{id}` 0 is the create form; any other id loads the record through the controller's `pact.FormExtendQuery` scope inside a transaction, and a record outside it is a 404. `Upload` receives a `pact.FormFieldUpload` with the transaction, the record (nil on the create form), the field, its options, the file name and the capped body, and returns the URL the element shows. The route answers 201 `{"data": {"url": "..."}}`; a `pact.FormFieldError` is a 422 on the field, a body over the cap a 413, and any other error the opaque 500. Checking the file (its size, its real type) is the type's job.
## The element