Repository navigation
fix(web-app-external): enable Collabora Save As / export to storage - #13906
Conversation
Collabora exposes Save As (export to another format, saved back to storage via WOPI PutRelativeFile) as a host-delegated operation: it posts a UI_SaveAs message and waits for the host to reply with the target filename via Action_SaveAs. The app-provider integration never opened that channel (it never sent Host_PostmessageReady and did not handle UI_SaveAs), so Save As silently did nothing while Rename (a native WOPI call) worked. Send Host_PostmessageReady once the editor iframe has loaded, request the grouped Save-As control via ui_defaults (SaveAsMode=group), and on UI_SaveAs prompt for the copy's name and reply with Action_SaveAs. The collaboration service already implements PutRelativeFile, so the copy is written into the same space respecting the user's permissions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: David Walter <david.walter@kiteworks.com>
|
Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a changelog item based on your changes. |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: David Walter <david.walter@kiteworks.com>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
DeepDiver1975
left a comment
There was a problem hiding this comment.
Reviewed as maintainer. The WOPI Save-As approach is correct and well-explained: Collabora delegates "Save As" to the host via UI_SaveAs/Action_SaveAs, the Host_PostmessageReady handshake is what opens that channel, and PutRelativeFile is already implemented server-side. The refactor of the old catchClickMicrosoftEdit into a single onAppMessage switch is a clean improvement (and now correctly registers/unregisters the listener and re-evaluates openAsPreview reactively rather than once at mount). The changelog fragment is present and follows changelog/TEMPLATE. CI is green. Nice work.
I have one requested change before approving, plus a test gap.
1. (requested) Origin check fails open while appUrl is unresolved
const onAppMessage = (event: MessageEvent) => {
if (unref(appOrigin) && event.origin \!== unref(appOrigin)) {
return
}
...appOrigin is a computed over appUrl, which is undefined until the async loadAppUrl task resolves; on parse failure it also yields ''. Because an empty appOrigin is falsy, the && short-circuits and the origin check is skipped entirely — every origin's message is dispatched. The message listener is attached in onMounted, which fires while the WOPI open_url POST is still in flight (network latency, the 425 "still processing" path, a slow IdP). During that window any page holding a reference to this window can post {"MessageId":"UI_SaveAs"} (→ unsolicited "Save a copy" modal) or {"MessageId":"UI_Edit"} (→ forced view→write reopen of the user's own file). It also disables the check for the whole session if the backend ever returns a non-absolute app_url.
Impact is low (no cross-origin exfiltration — postToApp correctly fails closed on empty origin and never uses targetOrigin:'*'; the actions only affect the user's own file). But this is exactly the fail-open pattern the recent embed-mode CSRF fix (#13844, security-embed-mode-postmessage-origin-validation.md) set out to eliminate, so it's worth holding the same line here. The fix is to fail closed:
if (\!unref(appOrigin) || event.origin \!== unref(appOrigin)) {
return
}No legitimate editor message can arrive before appUrl is set (the editor iframe can't have loaded yet), so rejecting until the origin is known is strictly correct and closes both the load-race window and the bad-app_url case.
2. (test gap) No unit coverage for the new handler
The only test delta is the two updated snapshots for the ui_defaults URL param. The new behavior — origin filtering in onAppMessage, UI_SaveAs → modal → Action_SaveAs payload, and Host_PostmessageReady on iframe load — has no unit test. The PR body credits the webOffice e2e suite for the end-to-end chain, which is great, but the origin-check logic (precisely the part flagged in #1) is cheap to assert at the unit level and would lock in the fail-closed behavior so it can't regress. Please add a small test that (a) drops a message from a foreign origin, (b) accepts UI_SaveAs from the editor origin and asserts the modal/Action_SaveAs payload, and (c) asserts Host_PostmessageReady is posted on @load.
Minor (non-blocking)
- The modal accepts any filename and posts it straight into
Action_SaveAs.Values.Filename. Server-sidePutRelativeFileis responsible for sanitizing the name (strip path separators / leading dots), which I trust it does — but a small client-side guard against empty///\names inonConfirmwould be reasonable defense-in-depth and avoids a pointless round-trip.
Happy to approve once the origin guard fails closed (#1); the test (#2) would ideally land in the same PR. Thanks!
… name Address review on #13906. - onAppMessage now fails closed: it rejects every editor message until the editor origin is known (appUrl resolved) and accepts only that exact origin, instead of skipping the check while appOrigin is empty. Closes the load-race window (the message listener is attached on mount, before the WOPI open_url POST resolves) and the non-absolute app_url case. - The Save-a-copy modal now validates the file name (live onInput + a confirm guard): empty names and any path separators are rejected before Action_SaveAs is posted. Server-side PutRelativeFile still sanitizes; this is defense in depth and avoids a pointless round-trip. - Add unit tests for the postMessage handling: messages before the origin is known are dropped, foreign-origin messages are dropped, UI_SaveAs from the editor origin opens the modal and posts Action_SaveAs on confirm, invalid names are rejected, and Host_PostmessageReady is posted on iframe load. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: David Walter <david.walter@kiteworks.com>
|
Thanks for the thorough review @DeepDiver1975 — all addressed in ed7188f. 1. (requested) Origin check now fails closed. if (!unref(appOrigin) || event.origin !== unref(appOrigin)) {
return
}This closes the load-race window (the listener is attached on mount, before the 2. (test gap) Unit tests added in
3. (minor) Client-side filename guard added. The modal now validates via Local: |
|
Re-reviewed 1. Origin guard now fails closed. 2. Tests added. The new The added No remaining concerns on the changed code. CI is green across the board. LGTM from my side. |
LukasHirt
left a comment
There was a problem hiding this comment.
Ran a multi-agent review (28 verifier agents, high effort) across the two changed files. Found 4 confirmed bugs / design issues that warrant changes before merge, plus 6 plausible hazards worth noting. Grouped by severity below.
Confirmed (request changes)
withSaveAsUiDefaultssilently no-ops whenui_defaultsalready in the URL — feature entirely broken for Collabora deployments where the server decorates the URL with its ownui_defaults(e.g.UIMode=notebookbar).searchParams.hasreturnstrue, the branch is skipped,SaveAsMode=groupis never appended, Collabora never sendsUI_SaveAs. Should append to the semicolon-delimited list instead of replacing.onSaveAshas noisReadOnlyguard —loadAppUrlalready short-circuits with a visible error for read-only files, butonSaveAsdispatches the modal regardless. When the user confirms,Action_SaveAsis posted; WOPI rejectsPutRelativeFilesilently; the modal closes normally. User sees success, no copy is created.- Collabora-specific protocol applied unconditionally to all WOPI providers —
withSaveAsUiDefaultsandonIframeLoad → Host_PostmessageReadyfire for every provider regardless of which editor is active.ui_defaults=SaveAsMode=groupis appended to everyapp_url, andHost_PostmessageReadyis posted on every iframe load. Should be gated onappNameindicating Collabora. validateSaveAsFilenameduplicates shared validator logic — already diverges fromgetNameErrorMsginuseFileActionsCreateNewFile/useFileActionsRename(those also reject.,.., and reserved names). Adding a third implementation means they drift further over time.
Confirmed (test gap)
- No test that
UI_Editis a no-op whenopenAsPreviewis false — the old static guard prevented this case entirely; now it's a conditional inside the switch. If the guard is accidentally inverted, any authenticated Collabora instance can force a write-mode reload of any file. No existing test would catch the regression.
Plausible hazards (worth addressing)
- Relative
app_urlsilently breaksUI_Edit—appOrigincomputes''for relative URLs; every postMessage (includingUI_Edit) is then dropped, so the Edit button stops working for any deployment where the WOPI server co-hosts on the same origin as the web app. - Silent
postToAppnull + modal dismissed as success — ifloadAppUrlrestarts betweenUI_SaveAsreceipt and the user pressing Save,appIframecan be transiently null;postToAppno-ops silently butModalWrapperstill closes the dialog, giving false positive feedback. onConfirmclosure over live reactive refs — in a SPA where the component is reused across resources without remounting, a resource-prop change while the modal is open rewritesappUrl/appIframe; the user's Save press dispatchesAction_SaveAsto the new document.UI_SaveAscan double-fire — no re-entrancy guard; rapid double-click of Collabora's export button opens two concurrent Save As dialogs and sends twoAction_SaveAsmessages, causing twoPutRelativeFilecalls.flushAppUrlhardcoded to 5 ticks instead offlushPromises— ifloadAppUrlgains another async step the count silently falls short, making tests intermittently unreliable.
…ew gaps Address the review feedback on the Save-As feature: append rather than overwrite the semicolon-delimited ui_defaults param so the grouped Save-As control still gets requested on deployments that already set ui_defaults, guard onSaveAs against read-only resources, restrict the ui_defaults request and Host_PostmessageReady handshake to the Collabora provider so other WOPI editors are unaffected, and reject reserved "." / ".." Save-As file names in addition to the existing empty/path-separator checks. Adds unit test coverage for each of these cases plus a regression test for UI_Edit no-op when openAsPreview is disabled.
Follow-up to fabe692: resolve a relative app_url from being resolved against the current origin so it doesn't permanently disable the postMessage origin check, guard against a second UI_SaveAs opening a duplicate modal, capture the target iframe/origin at the moment the modal is dispatched instead of re-reading live refs in onConfirm (which could point at a different resource by the time the user confirms), and surface an error instead of silently closing the modal when the editor iframe is unavailable. Adds unit test coverage for each case.
|
|
Re-reviewed after Confirmed (were blocking):
Plausible hazards (also addressed): Local |



Problem
Office documents opened in Collabora Online can be renamed but "Save As" / "Export As" silently does nothing — the copy is never written back into oCIS storage.
Root cause: Collabora implements "Save As" as a host-delegated operation. When the user triggers it, Collabora posts a
UI_SaveAsmessage and waits for the integration to reply with the target filename viaAction_SaveAs; only then does it render to the chosen format and call WOPIPutRelativeFile. The app-provider (web-app-external) never opened that postMessage channel — it never sentHost_PostmessageReadyand never handledUI_SaveAs(it only listened forUI_Edit). Rename works because Collabora performs it as a native WOPIRenameFilecall that needs no host involvement.Fix
In
web-app-external/src/App.vue:Host_PostmessageReadyonce the editor iframe has loaded — this is what makes Collabora start emitting its postMessage API (App_LoadingStatus,UI_SaveAs, …).ui_defaults=SaveAsMode=groupto the editor URL so Collabora shows the grouped Save-As control that delegates the target filename to the host (the parameter is Collabora-specific and ignored by other app providers).UI_SaveAs: prompt for the copy's name (the extension selects the export format) and reply withAction_SaveAs. Incoming messages are now origin-checked against the editor origin.No server change is needed — the collaboration service already implements
PutRelativeFile.Testing
web-app-externaltests pass; snapshot updated for theui_defaultsURL parameter.check:types, ESLint clean.ocis_full+ Collabora Online25.04.7.3.1:PutRelativeFilewrites the converted copy into the space (confirmed end-to-end: a real call created a.pdfnext to the source;PutFile: success→PutRelativeFileSuggested: success, HTTP 200).Host_PostmessageReadyon iframe load opens Collabora's full postMessage channel (App_LoadingStatus→Document_Loaded); without it Collabora stays silent.CheckFileInfoalready returnsUserCanNotWriteRelative:false+PostMessageOrigin.UI_SaveAs → Action_SaveAs → PutRelativeFilechain is exercised by the office web e2e suite (webOffice), which drives Collabora's Save-As UI.Risk
Low. Behaviour is additive: a new postMessage handshake + an extra UI param. The existing
UI_Edithandling is preserved (now also origin-checked). Non-Collabora app providers ignore theui_defaultsparameter.