Skip to content

feat(server): integrate OnlyOffice document editing#15148

Open
dadastory wants to merge 6 commits into
toeverything:canaryfrom
dadastory:feat/onlyoffice-pr
Open

feat(server): integrate OnlyOffice document editing#15148
dadastory wants to merge 6 commits into
toeverything:canaryfrom
dadastory:feat/onlyoffice-pr

Conversation

@dadastory

@dadastory dadastory commented Jun 24, 2026

Copy link
Copy Markdown

What

Adds optional OnlyOffice Document Server integration so users can view and edit office attachments (.docx / .xlsx / .pptx, …) directly from AFFiNE, gated behind an experimental
feature flag (off by default).

Why

Office documents are a common attachment type, but currently they can only be downloaded. This lets self-hosted instances (with an OnlyOffice Document Server) open them in-place for viewing, editing, reviewing,
commenting and form-filling.

How it works

  • Backend (packages/backend/server/src/plugins/onlyoffice/, new NestJS module):
    • Signs the OnlyOffice editor config (JWT, HS256) using a shared secret.
    • Public, token-authenticated download endpoint so the Document Server can fetch the blob server-side (the generic /blobs route requires a user session).
    • JWT-verified callback that writes edits back as content-addressed blobs (same sha256 scheme as blocksuite), never overwriting in place.
    • Per-attachment version manifest (switch / delete previous versions).
    • Standalone editor page (own window) — decoupled from the editor runtime, avoids in-app iframe/CSP issues.
    • 7 interaction modes (edit / review / comment / fill-forms / read-only / embedded / mobile), UI-language alignment, and share-permission alignment (read-only shares only get view modes; the backend enforces
  • Frontend (packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/, new):
    • Attachment toolbar entry + in-app version panel, injected via ToolbarModuleExtension (the same DI mechanism the built-in AI/image toolbars use).
    • Wired through the view manager and a feature flag — no blocksuite core schema changes.

Configuration

Enabled per-deployment via config / env (all default to disabled/empty):

Key Purpose
onlyoffice.enabled master switch
onlyoffice.documentServerUrl browser-facing Document Server URL
onlyoffice.internalUrl server-to-server URL (optional)
onlyoffice.callbackHost base URL the Document Server uses to reach AFFiNE
onlyoffice.jwtSecret shared JWT secret (must match the Document Server)

The UI entry only appears when the enable_onlyoffice experimental feature flag is turned on (off by default).

Scope / safety

  • Default off — zero behavior change unless explicitly enabled and configured.
  • No blocksuite core / schema / native changes.
  • Additive: one new backend module + one new view-extension + a feature flag and a few registration lines (app.module.ts, view manager, use-ai-specs.ts, lit-adaper.tsx, feature-flag/constant.ts).

Notes / open questions for maintainers

  • i18n: toolbar labels are plain English, consistent with the existing blocksuite attachment toolbar (which is also hardcoded English). Happy to wire them through the i18n pipeline if preferred.
  • Tests are not included yet — guidance welcome on the preferred test approach for this module.
  • Self-host deployment glue (Docker/compose for the Document Server service) is intentionally kept out of this PR; can be added to docs separately if desired.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added an “Open with OnlyOffice” action for supported office attachments (Word, Excel, PowerPoint), with edit/view options.
    • Enabled OnlyOffice support behind the enable_onlyoffice feature toggle.
    • Edits made in OnlyOffice now sync back to the workspace automatically, updating the attachment (and size when available).
    • Introduced an OnlyOffice version history panel to view versions, switch the active version, and delete versions when permitted.

Add OnlyOffice Document Server integration for editing office attachments
(docx/xlsx/pptx, ...), gated behind an experimental feature flag (default off).

- backend plugins/onlyoffice: signed editor config, token-authenticated file
  download for the Document Server, JWT-verified callback that writes edits
  back as content-addressed blobs, per-attachment version manifest, and a
  standalone editor page. Supports 7 interaction modes, UI-language alignment
  and share-permission alignment.
- frontend view-extensions/onlyoffice: attachment toolbar entry + in-app
  version panel, wired through a feature flag and the view manager. No
  blocksuite core schema changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dadastory dadastory requested a review from a team as a code owner June 24, 2026 03:41
@CLAassistant

CLAassistant commented Jun 24, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a backend OnlyOffice plugin with config, controller endpoints, service logic, and editor-page bootstrap, plus frontend feature-flag wiring, toolbar actions, and a version-history panel for attachment editing.

Changes

OnlyOffice Integration

Layer / File(s) Summary
Backend types, config, and module wiring
packages/backend/server/src/plugins/onlyoffice/types.ts, packages/backend/server/src/plugins/onlyoffice/config.ts, packages/backend/server/src/plugins/onlyoffice/index.ts, packages/backend/server/src/app.module.ts
Defines OnlyOffice data shapes and mode helpers, registers the OnlyOffice config module, adds the NestJS module, and includes it in the GraphQL server composition.
Backend service core flows
packages/backend/server/src/plugins/onlyoffice/service.ts
Implements token signing and verification, editor-config construction, callback processing, version manifest storage, save-result caching, force-save execution, and callback URL host validation.
Backend controller and editor page
packages/backend/server/src/plugins/onlyoffice/controller.ts, packages/backend/server/src/plugins/onlyoffice/editor-page.ts
Implements the OnlyOffice endpoints and the standalone editor HTML bootstrap that loads config, initializes DocsAPI, and posts save-back messages.
Frontend feature flag and view manager wiring
packages/frontend/core/src/modules/feature-flag/constant.ts, packages/frontend/core/src/blocksuite/manager/view.ts, packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx, packages/frontend/core/src/components/hooks/affine/use-ai-specs.ts, packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/index.ts
Adds the OnlyOffice feature flag, wires it through Blocksuite view configuration, registers the view extension, and threads the flag into editor-spec hooks.
Frontend toolbar and version panel
packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/toolbar.ts, packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/version-panel.ts
Implements the attachment toolbar action, standalone editor launch, save-back handling, version-history launch, and the version-history modal UI.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested reviewers

  • darkskygit
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding OnlyOffice document editing integration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

@coderabbitai coderabbitai Bot added mod:component mod:infra Environment related issues and discussions labels Jun 24, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/backend/server/src/plugins/onlyoffice/controller.ts`:
- Around line 158-176: The forcesave endpoint and delete-version endpoint (at
lines 215-235) are using `@Get` decorators for operations that mutate state, which
violates REST conventions and creates security vulnerabilities. Change the `@Get`
decorators to `@Post` decorators for both the forcesave method and the
delete-version method. Then update all callers of these endpoints in
packages/backend/server/src/plugins/onlyoffice/editor-page.ts,
packages/backend/server/src/plugins/onlyoffice/versions-page.ts, and
packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/version-panel.ts
to send requests using method: 'POST' instead of GET.

In `@packages/backend/server/src/plugins/onlyoffice/editor-page.ts`:
- Around line 77-104: The polling loop that waits for the save result may
complete without ever receiving a blobId (in case of slow callbacks or
failures), but the code still proceeds to close the editor window, causing the
user's edits to appear lost. Add a flag variable (e.g., saved) that tracks
whether the postMessage with the new blobId was successfully sent inside the
condition checking jr && jr.blobId. Then, only close the editor window if this
flag is true, ensuring the window remains open when save confirmation is
missing.

In `@packages/backend/server/src/plugins/onlyoffice/service.ts`:
- Around line 555-625: The recordVersion method performs a non-atomic
read-modify-write sequence on manifest data without any serialization mechanism,
allowing concurrent status-2 and status-6 callbacks to race and overwrite each
other's changes. Add a locking or serialization mechanism that ensures only one
recordVersion call executes at a time for a given combination of workspaceId,
docId, and blockId. Use a Map-based lock (keyed by these three parameters) or a
task queue to serialize manifest updates and prevent concurrent modifications
from corrupting the version history.
- Around line 596-598: The hardDeleteBlob method calls at multiple locations
(around lines 596-598, 616-618, and 631-637) are deleting content-addressed
blobs without verifying that no other attachments or documents still reference
them. Before calling hardDeleteBlob for the stale blob, add a reference safety
check to ensure the blob is not shared by any other documents or attachments in
the workspace. Only proceed with the hard deletion if the blob has no remaining
references, otherwise use a soft delete approach or implement a garbage
collection mechanism that safely handles shared blobs.
- Around line 711-719: The fetch call to the OnlyOffice CommandService.ashx
endpoint does not validate the response status, so non-2xx responses are
silently treated as success. After the fetch call (within the try block), check
if the response status indicates failure by validating the response.ok property
or response.status, and if the response indicates an error, throw an error with
appropriate details. This ensures that failed force-save operations are properly
caught by the existing catch block and logged as warnings instead of being
silently treated as successful.
- Around line 659-664: The deleteVersion method currently deletes any provided
blobId without verifying it exists in the manifest.versions array first,
creating a security vulnerability where arbitrary workspace blobs can be
deleted. After filtering manifest.versions to remove the matching blobId and
before calling this.blobStorage.delete, add a check to verify that the filter
actually removed an entry (meaning the blobId was present in manifest.versions).
Only proceed with the blob deletion if the blobId was found and removed from the
manifest, otherwise reject the operation to prevent deletion of blobs not
associated with this attachment.

In `@packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/toolbar.ts`:
- Around line 143-152: The onMessage message handler validates the origin but
does not verify that the message came from the specific popup window instance,
creating a security vulnerability where any same-origin window could send a
matching affine-onlyoffice-saved message and mutate the attachment reference.
Add a check for e.source in the validation condition of the onMessage function
to ensure the message originated from the specific popup window that was created
(you will need to ensure the popup window reference is captured when the window
is opened and then compare e.source against that reference in the validation
logic). This same source validation should also be applied at the other location
mentioned in the comment (lines 165-166).

In
`@packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/version-panel.ts`:
- Around line 151-167: The _load() method sets the error property when a fetch
fails but never clears it when the load succeeds, causing stale error messages
to persist in the UI even after a successful version load. Add code at the
beginning of the try block in the _load() method to reset this.error to null or
an empty string before attempting to load the versions. This ensures that
successful loads clear any previous error state.
- Around line 247-255: The "Switch to" button in the version panel is being
rendered without checking write permissions, exposing a non-functional action in
read-only contexts. In the conditional rendering block around the switch
statement (where isCur determines whether to show "Current" or the switch
button), add a check for this.canWrite to the condition that displays the button
with the `@click`=${() => this._switch(v)} handler. The button should only render
when both canWrite is true AND isCur is false, so the button rendering logic
needs to be gated by the canWrite permission check alongside the existing isCur
check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f9b14f90-9694-453e-ac57-6d432f522936

📥 Commits

Reviewing files that changed from the base of the PR and between c1c19be and d63ae64.

📒 Files selected for processing (15)
  • packages/backend/server/src/app.module.ts
  • packages/backend/server/src/plugins/onlyoffice/config.ts
  • packages/backend/server/src/plugins/onlyoffice/controller.ts
  • packages/backend/server/src/plugins/onlyoffice/editor-page.ts
  • packages/backend/server/src/plugins/onlyoffice/index.ts
  • packages/backend/server/src/plugins/onlyoffice/service.ts
  • packages/backend/server/src/plugins/onlyoffice/types.ts
  • packages/backend/server/src/plugins/onlyoffice/versions-page.ts
  • packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx
  • packages/frontend/core/src/blocksuite/manager/view.ts
  • packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/index.ts
  • packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/toolbar.ts
  • packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/version-panel.ts
  • packages/frontend/core/src/components/hooks/affine/use-ai-specs.ts
  • packages/frontend/core/src/modules/feature-flag/constant.ts

Comment thread packages/backend/server/src/plugins/onlyoffice/controller.ts Outdated
Comment thread packages/backend/server/src/plugins/onlyoffice/editor-page.ts Outdated
Comment thread packages/backend/server/src/plugins/onlyoffice/service.ts
Comment thread packages/backend/server/src/plugins/onlyoffice/service.ts
Comment thread packages/backend/server/src/plugins/onlyoffice/service.ts Outdated
Comment thread packages/backend/server/src/plugins/onlyoffice/service.ts Outdated
- use POST for mutating endpoints (forcesave, delete-version) + update callers
- editor page: keep window open and surface error if save-back is unconfirmed
- serialize per-attachment manifest read-modify-write to avoid lost history
- soft-delete superseded/removed version blobs (content-addressed, may be shared)
- deleteVersion: reject blob ids not present in the attachment manifest
- forcesave: treat non-2xx / error responses as failures
- toolbar: bind save-back postMessage to the actual popup (check e.source)
- version panel: reset error after a successful load; gate "Switch to" on write
- remove dead standalone versions-page (replaced by the in-app panel)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai coderabbitai Bot added the mod:env label Jun 24, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/backend/server/src/plugins/onlyoffice/service.ts (1)

781-785: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed when no allowed Document Server host is configured.

When neither documentServerUrl nor internalUrl parses to a valid host, allowedHosts is empty and the allowedHosts.length && guard skips validation entirely, letting applyCallback fetch an arbitrary downloadUrl. Because the fetch here is the plain globalThis.fetch (intentionally not SSRF-guarded, per the comment at lines 409-411), this becomes an SSRF vector against internal services if the integration is ever in a partially-configured state. Reject instead of allowing through.

🛡️ Fail closed on empty allowlist
-    if (allowedHosts.length && !allowedHosts.includes(target.host)) {
+    if (!allowedHosts.length || !allowedHosts.includes(target.host)) {
       throw new BadRequest(
         `OnlyOffice callback url host not allowed: ${target.host}`
       );
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/backend/server/src/plugins/onlyoffice/service.ts` around lines 781 -
785, The validation guard in the OnlyOffice callback host check currently skips
validation entirely when allowedHosts is empty (due to the allowedHosts.length
&& condition), which creates an SSRF vulnerability. Remove the
allowedHosts.length check from the condition so that the validation always runs
regardless of whether allowedHosts is populated, ensuring the BadRequest is
thrown when allowedHosts is empty or when the target.host is not in the
allowedHosts array. This ensures the code fails closed when no Document Server
hosts are configured rather than allowing arbitrary downloadUrls to be fetched.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/backend/server/src/plugins/onlyoffice/service.ts`:
- Around line 24-26: The manifestLocks in-memory Map only serializes operations
within a single Node process, which causes race conditions and data corruption
in multi-instance deployments when concurrent status-2/status-6 callbacks for
the same attachment land on different replicas. Replace the in-memory
Promise-based locking mechanism in the manifestLocks field with the distributed
Mutex primitive (backed by Redis via Locker) that already exists in the codebase
and is used by other services like payment and copilot. Apply this distributed
Mutex to both the manifest lock operations and the serialization logic
referenced in lines 544-565 to ensure cross-instance consistency. Alternatively,
if single-instance deployment is required, document this constraint clearly in
the class documentation.

---

Outside diff comments:
In `@packages/backend/server/src/plugins/onlyoffice/service.ts`:
- Around line 781-785: The validation guard in the OnlyOffice callback host
check currently skips validation entirely when allowedHosts is empty (due to the
allowedHosts.length && condition), which creates an SSRF vulnerability. Remove
the allowedHosts.length check from the condition so that the validation always
runs regardless of whether allowedHosts is populated, ensuring the BadRequest is
thrown when allowedHosts is empty or when the target.host is not in the
allowedHosts array. This ensures the code fails closed when no Document Server
hosts are configured rather than allowing arbitrary downloadUrls to be fetched.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2a08b73f-762c-43ea-86dc-4c22b14b60c3

📥 Commits

Reviewing files that changed from the base of the PR and between d63ae64 and 1364846.

📒 Files selected for processing (5)
  • packages/backend/server/src/plugins/onlyoffice/controller.ts
  • packages/backend/server/src/plugins/onlyoffice/editor-page.ts
  • packages/backend/server/src/plugins/onlyoffice/service.ts
  • packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/toolbar.ts
  • packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/version-panel.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/toolbar.ts
  • packages/frontend/core/src/blocksuite/view-extensions/onlyoffice/version-panel.ts
  • packages/backend/server/src/plugins/onlyoffice/editor-page.ts

Comment thread packages/backend/server/src/plugins/onlyoffice/service.ts Outdated
Replace the in-process promise-chain lock with the Redis-backed Mutex so
manifest read-modify-write is serialized across server instances, not
just within a single Node process. Prevents concurrent status-2/6 save
callbacks landing on different replicas from corrupting version history.
@dadastory

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 43.23933% with 785 lines in your changes missing coverage. Please review.
✅ Project coverage is 51.86%. Comparing base (c1c19be) to head (018593e).
⚠️ Report is 1 commits behind head on canary.

Files with missing lines Patch % Lines
...s/backend/server/src/plugins/onlyoffice/service.ts 21.30% 602 Missing ⚠️
...ackend/server/src/plugins/onlyoffice/controller.ts 30.88% 179 Missing ⚠️
...ges/backend/server/src/plugins/onlyoffice/types.ts 97.14% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           canary   #15148      +/-   ##
==========================================
- Coverage   58.09%   51.86%   -6.23%     
==========================================
  Files        3296     3235      -61     
  Lines      188605   187371    -1234     
  Branches    27217    25104    -2113     
==========================================
- Hits       109567    97188   -12379     
- Misses      75273    86869   +11596     
+ Partials     3765     3314     -451     
Flag Coverage Δ
server-test 66.54% <43.23%> (-10.51%) ⬇️
unittest 32.96% <ø> (-2.10%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- Import the repeat directive from lit/directives/repeat.js instead of
  lit-html (not a direct dependency) to satisfy the
  import-x/no-extraneous-dependencies lint rule.
- Document that OnlyOffice drops embedded fonts on save, so the stored
  blob can be much smaller than the original without any data loss.
@dadastory dadastory changed the title feat(onlyoffice): integrate OnlyOffice document editing feat(server): integrate OnlyOffice document editing Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:core app:server mod:component mod:env mod:infra Environment related issues and discussions

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants