feat(parity): real getHTML export, padCopy/padRemove hooks, public-status guard, listAuthors filter#284
Conversation
…atus guard, listAuthors filter Next parity round vs the original etherpad: getHTML: - the API getHTML returned fake HTML (it string-replaced newlines with <br> and dropped all formatting). It now renders through the existing HTML exporter (GetPadHTMLDocument), preserving bold/italic/underline, links, lists, headings and line markers, mirroring upstream getHTML which delegates to exportHtml.getPadHTMLDocument. padCopy / padRemove plugin hooks: - ported the two missing pad-lifecycle hooks. padRemove fires from Manager.RemovePad (the single removal choke point: deletePad, movePad source removal and force-overwrite of a copy destination), capturing the loaded pad before deletion. padCopy fires from copyPad, copyPadWithoutHistory and movePad with the source and destination pads. public-status group guard: - getPublicStatus/setPublicStatus now reject non-group pads (id without a $), mirroring the original checkGroupPad guard, instead of silently operating on pads that can never be public. listAuthorsOfPad: - filter out the synthetic SystemAuthorId (a.etherpad-system), used for unattributed inserts, so it no longer surfaces as a real contributor through the public API (matches upstream's Pad.SYSTEM_AUTHOR_ID filter). Tests: padCopy/padRemove hook firing, public-status non-group rejection, system-author exclusion; existing public-status tests updated to use valid group pad ids. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
Code Review by Qodo
1. padRemove pad can be nil
|
PR Summary by QodoFix getHTML export, add padCopy/padRemove hooks, guard publicStatus, filter system author WalkthroughsDescription• Render getHTML via the real HTML exporter to preserve rich formatting. • Add missing padCopy/padRemove plugin hooks and fire them from copy/move/remove paths. • Enforce group-pad-only publicStatus and hide the synthetic system author from listAuthorsOfPad. Diagramgraph TD
A["Pad API"] --> B["Copy/Move & Status"] --> G{"Group pad?"}
G -->|"yes"| E["Pad manager"] --> F["Plugin hooks"]
G -->|"no"| H["400 NotAGroup"]
A --> C["HTML/Authors ops"] --> E
C --> D["HTML exporter"] --> E
High-Level AssessmentThe following are alternative approaches to this PR: 1. Pass dstPad through copy/move flow (avoid reload in padCopy hook)
2. Centralize publicStatus group-pad validation in shared middleware/helper
Recommendation: Current approach is sound for parity: using the existing HTML exporter minimizes drift and preserves formatting, firing padRemove from PadManager.RemovePad makes deletion hook coverage comprehensive, and adding an explicit group-pad guard matches upstream semantics. If padCopy hook performance becomes a concern, consider threading dstPad through the copy/move code paths to avoid reloading it for the hook context. File ChangesEnhancement (4)
Bug fix (2)
Tests (1)
|
| // Capture the loaded pad (if any) before deletion so the padRemove hook can | ||
| // hand listeners the pad context, mirroring the original Etherpad which | ||
| // fires padRemove from Pad.remove() with `this`. | ||
| removedPad := m.globalPadCache.GetPad(padID) | ||
|
|
||
| if err := m.store.RemovePad(padID); err != nil { | ||
| return err | ||
| } | ||
| m.globalPadCache.DeletePad(padID) | ||
| m.padList.RemovePad(padID) | ||
|
|
||
| m.hook.ExecuteHooks(hooks.PadRemoveString, pad.Remove{ | ||
| Pad: removedPad, | ||
| PadId: padID, | ||
| }) |
There was a problem hiding this comment.
1. Padremove pad can be nil 🐞 Bug ☼ Reliability
Manager.RemovePad only captures the pad from the in-memory cache and passes it to the new padRemove hook, so ctx.Pad will be nil whenever the pad wasn't already loaded. This can happen on force-overwrite copy destinations (removed without loading), causing plugins to panic on nil dereference or lose access to removed pad state.
Agent Prompt
### Issue description
`Manager.RemovePad()` fires the new `padRemove` hook with a `Pad` value taken only from `globalPadCache`. If the pad was never loaded (common for force-overwrite copy destinations), the hook receives `ctx.Pad == nil`, which can break plugin expectations and can crash the process if a hook callback dereferences it.
### Issue Context
The PR intent says the hook should capture the loaded pad before deletion (mirroring upstream). Today, `prepareCopyDestination()` can call `RemovePad(destinationID)` without ever loading that pad into the cache.
### Fix Focus Areas
- lib/pad/padManager.go[130-146]
- lib/api/pad/copyMove.go[33-44]
### Suggested fix approach
- In `Manager.RemovePad`, if `removedPad == nil`, check existence (`m.store.DoesPadExist(padID)`), and if it exists, load it (`m.GetPad(padID, nil, nil)`) *before* deleting so the hook gets a non-nil pad context.
- Alternatively (or additionally), in `prepareCopyDestination` (force path), load the destination pad via `GetPadSafe(destinationID, true, nil, nil, initStore.PadManager)` before calling `RemovePad`.
- Ensure you do not accidentally create a new pad when deleting a non-existent pad (so existence check must gate any load).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Next parity round vs the original etherpad, building on #276/#279/#280.
getHTML
The HTTP API
getHTMLreturned fake HTML: it string-replaced\nwith<br>and dropped every other formatting attribute (bold/italic/underline, links, lists, headings, line markers). It now renders through the existing HTML exporter (GetPadHTMLDocument), mirroring the original EtherpadgetHTML, which delegates toexportHtml.getPadHTMLDocument.padCopy / padRemove plugin hooks
Ported the two missing pad-lifecycle hooks:
Manager.RemovePad— the single removal choke point coveringdeletePad,movePadsource removal, and force-overwrite of a copy destination — capturing the loaded pad before deletion.copyPad,copyPadWithoutHistoryandmovePadwith the source and destination pads, matching the upstream{srcPad, dstPad}context.public-status group guard
getPublicStatus/setPublicStatusnow reject non-group pads (id without a$) with a 400, mirroring the originalcheckGroupPadguard, instead of silently operating on pads that can never be public.listAuthorsOfPad
Filters out the synthetic
SystemAuthorId(a.etherpad-system), used for unattributed inserts (HTTP API writes without an authorId, server-side imports). It is a changeset-bookkeeping detail, not a real contributor, so it no longer surfaces through the public API — matching upstream'sPad.SYSTEM_AUTHOR_IDfilter.Tests
🤖 Generated with Claude Code