SPDX-License-Identifier: AGPL-3.0-or-later
Base: /apps/etherpad_nextcloud
-
GET /- Controller:
ViewerController::showPad - Query:
file=/path/to/file.pad - Purpose: compatibility entry route that redirects to native Files viewer URL.
- Controller:
-
GET /by-id/{fileId}- Controller:
ViewerController::showPadById - Purpose: compatibility entry route via file ID; redirects to native Files viewer URL.
- Controller:
-
GET /embed/by-id/{fileId}- Controller:
EmbedController::showById - Purpose: minimal authenticated embed page for trusted same-site / trusted-origin integrations.
- Behavior:
- requires a logged-in Nextcloud user
- validates that
fileIdresolves to an accessible.padfile in the user's file tree - renders a blank embed page that internally calls
open-by-id - injects CSRF token manually into the blank template because this layout does not receive the normal
OC.requestTokenbootstrap - if open fails with
code: missing_frontmatter, the embed page retries once afterinitialize-by-id/{fileId} - sets route-specific
frame-ancestorsfrom admin-configured trusted embed origins
- Host message contract:
- accepted incoming messages from trusted origins:
epnc:host-visibleepnc:host-hiddenepnc:host-before-closeepnc:host-sync-now
- emitted replies to the sending host origin:
epnc:sync-flush-startedepnc:sync-flush-finishedepnc:sync-flush-failed
- intended use:
- host sends
epnc:host-before-close - waits briefly for
epnc:sync-flush-finishedorepnc:sync-flush-failed - only then unmounts the iframe
- host sends
- accepted incoming messages from trusted origins:
- Controller:
-
GET /embed/create-by-parent/{parentFolderId}- Controller:
EmbedController::createByParent - Query:
name(required)accessMode(public|protected, optional, defaultprotected)
- Purpose: minimal authenticated create launcher page for trusted same-site / trusted-origin integrations.
- Behavior:
- requires a logged-in Nextcloud user
- validates that
parentFolderIdresolves to an accessible writable folder in the user's file tree - renders a blank page that internally calls
POST /api/v1/pads/create-by-parentsame-origin with CSRF token - injects CSRF token manually into the blank template because this layout does not receive the normal
OC.requestTokenbootstrap - on success redirects itself to the returned
embed_url - sets route-specific
frame-ancestorsfrom admin-configured trusted embed origins
- Controller:
A share token identifies the share, not a file inside it. For a share of a single file that is enough. For a share of a folder the request has to say which file it means, and there are two ways to do that:
fileId=<int>- the file's Nextcloud id. Preferred, and what the app's own viewer sends.file=/subfolder/file.pad- the path inside the shared folder. Still supported, so links written before ids were accepted keep working.
Prefer the id where you have one. A name has to survive being put in a query string, and a query string spells a space two ways: A+B.pad and A B.pad arrive as the same text. An id has one spelling.
A request is answered when the file it names is a .pad the share actually contains and the visitor may read. It is refused when:
- the id is not a positive integer, including when
fileId=is sent empty; - the id names no file inside this share, whether it exists elsewhere or not at all;
- both are sent and they name different files.
A folder named in place of a file is refused as not being a .pad (400), not as missing (404).
In none of those cases does the request fall through to the other locator. A refused id is not retried as a path, because "the id did not work, so something else opened" is the outcome ids exist to prevent.
When both are sent for a folder share, the path is compared in full: A.pad at the top of the share and Sub/A.pad are different files. A single-file share has no path inside it, so there the file's name is what a path can name - and without an id it is ignored entirely, as it always has been.
-
GET /public/{token}- Controller:
PublicViewerController::showPad - Query (folder share):
file=/subfolder/file.pad- name only, and deliberately: this route builds no viewer address of its own, it hands over to Nextcloud's own share page. - Purpose: compatibility route for public shares; redirects to
/s/{token}with selected file. - UX behavior:
- Errors are rendered as
noviewertemplate (not raw JSON). - Error page includes back-link to share entry page (
/s/{token}).
- Errors are rendered as
- Controller:
-
GET /api/v1/public/open/{token}- Controller:
PublicViewerController::openPadData - Query:
fileId=<int>orfile=/subfolder/file.pad- see "Naming the file in a public share" above. - Purpose: resolves a
.padfile inside a public share for the native viewer. - Result:
- writable protected share: Etherpad URL plus one
sessionIDSet-Cookieheader - read-only protected share:
is_readonly_view=true, emptyurl, and acontent_urlthe viewer loads the pad from; no Etherpad session cookie - public/external pad share: regular public Etherpad URL, plus
content_urlfor external pads
- writable protected share: Etherpad URL plus one
- Controller:
-
GET /api/v1/public/content/{token}- Controller:
PublicViewerController::padContent - Query:
fileId=<int>orfile=/subfolder/file.pad- see "Naming the file in a public share" above. - Purpose: the pad's current content for the read-only view of a public share.
- Result: sanitized
htmlplusis_empty; answeredno-store. - Behavior: resolves the share and re-checks the
.padbinding on every call, so a retry cannot outlive the access it was granted under. - The
content_urlhanded out by the open endpoint carries the id that open resolved to, so a file renamed or moved inside the share between the two requests is still the one answered for.
- Controller:
Admins can switch either pad type off (see the admin settings). POST /pads
and POST /pads/create-by-parent refuse a disabled accessMode with 403.
Pads that already exist are unaffected.
POST /pads/from-template and the initialize endpoints do not refuse a
disabled mode — they create the pad in the enabled mode instead, so a
template or a .pad that arrived outside the UI stays usable. They only
return 403 when no pad type is enabled at all. Note this can widen access:
a protected template becomes a public pad when protected pads are off.
The refusal carries code: pad_type_disabled, plus access_mode naming the
disabled type — that field is absent when no pad type is enabled at all.
Branch on the code rather than on the message text.
POST /pads/from-url is deliberately exempt: external pads are governed
solely by the separate external-pad policy, not by these two settings.
-
POST /api/v1/pads- Controller:
PadCreateController::create - Params:
file(required)accessMode(public|protected, optional, defaultprotected)
- Result: creates pad, file, and binding.
- The app's own UI no longer calls this: a pad type is picked in Nextcloud's template picker, which creates the file itself. The endpoint stays for API consumers and behaves as before.
- Controller:
-
POST /api/v1/pads/create-by-parent- Controller:
PadCreateController::createByParent - Params:
parentFolderId(required, Nextcloud folder/file ID of the writable target folder)name(required, filename base;.padsuffix is appended if missing)accessMode(public|protected, optional, defaultprotected)
- Purpose: creates a managed
.padfile inside an existing parent folder without requiring the client to construct a full path string. - Result includes:
filefile_idparent_folder_idpad_idaccess_modepad_urlviewer_urlembed_url
- Intended use:
- trusted same-origin launcher pages inside Nextcloud
- not direct server-side cross-app mutation without a real Nextcloud user session
- Controller:
-
POST /api/v1/pads/from-url- Controller:
PadCreateController::createFromUrl - Params:
file(required)padUrl(required, absolutehttpsURL with/p/{padId})
- Purpose: creates
.padfor public Etherpad links from external servers. - External
.padfiles are file-only metadata/snapshot records and do not create rows inep_pad_bindings. - Security rules:
- public pad URLs only
- no GroupPad IDs (
g.<group>$<name>) - no local/private/reserved target addresses (DNS/IP checks)
- DNS result is pinned for the outbound fetch (rebinding mitigation)
- external
/export/txtresponses are size-limited (5 MiB hard limit) - external sync accepts only safe text-oriented response content-types
- Controller:
-
POST /api/v1/pads/from-template- Controller:
PadCreateController::createFromTemplate - Params:
file(required) — target path. Body placeholders ({{date}},{{user}}etc.) are resolved server-side.templateFileId(required) — id of any.padin the user's userspace; doesn't have to live in the Templates folder.
- Purpose: create-from-template path for custom frontends that need filename templating or want to pick the source file outside
/Templates. Bypasses NC'sTemplateManager. - Behaviour: resolves
{{...}}infileand template body, provisions a fresh Etherpad pad, writes the new.padcontent + snapshot, creates the binding. Returnsviewer_urlalongside the regular create response shape. - Errors: 400 (non-pad template / external template / empty), 404 (template id not found in userspace), 409 (filename collision).
- Controller:
-
POST /api/v1/pads/open- Controller:
PadSessionController::open - Params:
file=/path/file.pad - Result: secure open URL.
- Behavior: read-only (no auto-mutation of
.padmetadata), CSRF-protected. - Protected mode: response includes one Etherpad session
Set-Cookieheader.
- Controller:
-
POST /api/v1/pads/open-by-id- Controller:
PadSessionController::openById - Params:
fileId=<int> - Result: secure open URL via stable Nextcloud
fileId. - Behavior: read-only (no auto-mutation of
.padmetadata), CSRF-protected. - Protected mode: response includes one Etherpad session
Set-Cookieheader.
- Controller:
-
GET /api/v1/pads/content/{fileId}- Controller:
PadSessionController::contentById - Purpose: the pad's current content for this app's read-only view.
- Result: sanitized
htmlplusis_empty; answeredno-store. Both fields are always present — a reader must be able to tell an empty pad from an answer that carries no content. - Behavior: re-resolves the file and re-checks the
.padbinding on every call. Own pads are read over the Etherpad API, pads on other servers over their public HTML export. Either way the read stops at 5 MiB and answers400withcode: pad_too_largepast it.
- Controller:
-
POST /api/v1/pads/initialize- Controller:
PadSessionController::initialize - Params:
file=/path/file.pad - Purpose: explicit frontmatter initialization for empty/legacy
.padfiles.
- Controller:
-
POST /api/v1/pads/initialize-by-id/{fileId}- Controller:
PadSessionController::initializeById - Purpose: explicit frontmatter initialization by stable Nextcloud
fileId.
- Controller:
-
GET /api/v1/pads/meta-by-id/{fileId}- Controller:
PadSessionController::metaById - Purpose: read-only metadata endpoint for external UIs that need stable file context without triggering open/session bootstrap.
- Result includes:
is_padis_pad_mimefile_idnamepathaccess_modeis_externalpad_idpad_urlpublic_open_urlviewer_urlembed_url
- Controller:
-
GET /api/v1/pads/resolve- Controller:
PadSessionController::resolveById - Query:
fileId=<int>(preferred)file=/path/file.pad(path fallback)
- Result: MIME/path/viewer target for files frontend.
- Controller:
-
POST /api/v1/pads/sync/{fileId}- Controller:
PadLifecycleController::syncById - Optional query:
force=1 - Result: snapshot sync Etherpad ->
.pad(updatedorunchanged). force=1requests an immediate upstream re-check, but unchanged snapshots are still not rewritten. A pad behind the snapshot whose content differs is written.- A pad Etherpad made anew in place of the file's - without a single revision, with other text than the file saved - is never written over it:
400withcode=pad_missing, as an open answers, and the file keeps its content forrecover-from-snapshot. - External pads:
- Sync uses public text export only (
/export/txt) based onpad_url. - HTML is not imported for external pads.
- No DB binding is required; the external target is validated from
.padfrontmatter.
- Sync uses public text export only (
- Controller:
-
GET /api/v1/pads/sync-status/{fileId}- Controller:
PadLifecycleController::syncStatusById - Result:
status=syncedifsnapshot_rev >= current_revstatus=out_of_syncifsnapshot_rev < current_revstatus=unavailablefor external pads without safe revision lookup- External pads return
unavailablebecause the app intentionally does not keep revision state for remote servers.
- Controller:
-
POST /api/v1/pads/recover-from-snapshot/{fileId}- Controller:
PadLifecycleController::recoverByFileId - Purpose: manual recovery entry point for
.padfiles that ended up without a binding row (WebDAV backup restore,occ files:scan, direct DB intervention, file copy), and for a file whose active row names a pad Etherpad has lost (pad_missing). Reuses the paths a restore from the trash takes: "frontmatter → fresh pad" for a file without a row, the replacement of a pad that is gone for the other, which moves the row onto the new pad. Etherpad is asked again here whether the pad is lost. - Result:
200withstatus=restored,old_pad_id,new_pad_idon success. Always provisions a fresh pad —pad_idfrom frontmatter is never reused.403withmessagewhen the user may not change the file, a read-only share say: a recovery writes the file. Refused before Etherpad is asked.409withstatus=skipped+reason=external_padfor external (ext.*) frontmatter; recovery doesn't apply there.409withstatus=skipped+reason=file_movedorreason=file_changedwhen the file moved, or was written, while the new pad was seeded: the new pad is let go, nothing is written, and a retry starts from what the file holds then.409withmessageand thePadAlreadyHasBindingExceptionmapping if the file has a row and its pad is not lost: Etherpad has it, the row waits, or it names another pad than the file.503withretryablewhen Etherpad does not answer.
- Controller:
-
GET /api/v1/pads/find-original/{fileId}- Controller:
PadLifecycleController::findOriginalByFileId - Purpose: look up whether the orphan's frontmatter
pad_idis bound to another.padthe requester can read. Used by the recovery UI to offer "Open the original" when a copy is detected. - Result:
200with{ found: true, file_id, path, viewer_url }when the lookup hits and the bound file is readable by the requester.200with{ found: false }for every miss path (no row, ext.* pad id, a binding whose file was deleted for good (pending_delete), binding for a file not addressable in the requester's userspace, unparseable frontmatter, orphan itself not readable, self-loop). Payload shape and status are intentionally identical so the endpoint cannot be used to probe for binding rows that belong to other users.
- Controller:
-
POST /api/v1/admin/settings- Controller:
AdminController::saveSettings - Auth: admin only
- Stores Etherpad and security settings, including:
etherpad_host(public/browser base URL)etherpad_api_host(optional internal API URL; fallback toetherpad_host)delete_pad_with_file(yes|no)
- Result includes
checkswith the singleprotected_padsline, recomputed from the saved values in the same shape the health check uses — so the settings page refreshes that verdict without a separate connection test, and clears results the save invalidated.
- Controller:
-
POST /api/v1/admin/health- Controller:
AdminController::healthCheck - Auth: admin only
messagesummarises the run — "All checks passed." or a note that some settings need attention. It deliberately does not say "successful": the request succeeding says nothing about the configuration.- Result includes:
-
host -
api_host -
api_version -
latency_ms -
target -
pending_delete_count— files deleted for good whose pads the sweep has yet to delete -
session_cookie_release— the Etherpad release the open path is going by, which can differ from the one this run probed -
checks— one entry per verified part, so a failure points at the field that caused it:api,api_key,base_url,session_cookieandprotected_pads. Each hasid,status(ok|warning|skipped),label,detailandfield, all already translated for display.fieldnames the form input the line belongs to, so the result can be rendered at that input; it is empty when the line belongs to no single field.apiandapi_keyboth come from onecheckTokencall, which proves the address answers and the key is accepted and nothing else. Etherpad implementscheckTokenas an empty function, so a passing line says nothing about whether the pad store can be read. A cheap storage probe would be possible —getRevisionsCountreads one key and answerscode: 1, padID does not existfor an absent pad, which is a pass for that purpose — but no check does it today. A green panel is not a statement about Etherpad's storage.base_urlperforms a short GET against the browser-facing Etherpad URL, which nothing else contacts. Unlike the API host it gets no local-address exemption — that exemption is what would let an admin-supplied address probe the server's own network. Redirects are not followed and the body is not buffered, since neither is needed for a status code. The protection is Nextcloud's, soallow_local_remote_servers=truedisables it instance-wide.session_cookiereaches out as well: it reads Etherpad's/healthfor the release the cookie decision depends on.The line is never an error. Nextcloud may be unable to reach the public URL by design (split-horizon DNS, egress firewall), and a blocked local address is reported as unverified rather than broken, since an internal deployment may still serve it to users. It runs even when the base URL matches the API URL, because the API call may succeed against a loopback address no browser can use; both lines then land on the same field, where the more severe verdict is the one shown.
-
protected_pads— the same verdict as theprotected_padsline inchecks, but machine-readable for support and other clients, ornullwhen protected pads are switched off. A failing verdict does not fail the connection test: the API can be reachable while protected pads cannot work. Fields:ok,status(ok|warning|unknown),reason,cookie_domain,cookie_domain_source(configured|derived|host_only),nextcloud_host,etherpad_host,message.See "Check iframe and cookie setup for protected pads" in
README.mdfor why the two hosts must share a parent domain. The check performs no I/O; public suffixes are recognised by a conservative heuristic rather than a Public Suffix List lookup, socommon_parent_may_be_public_suffixis a warning rather than a verdict.
-
- A failed connection test answers
200withok: false, amessageand, where the cause can be attributed,field— the same shape validation errors use, so the page marks the input rather than reporting only at the bottom. No other fields are present on that response. The status is deliberately not a5xx: the verdict is about the configured Etherpad and not about this request, and a gateway status invites a reverse proxy to replace the body that carries the reason. Readok, never the status.
- Controller:
-
GET /api/v1/admin/templates- Controller:
AdminController::listPadTemplates - Auth: admin only
- Result:
templates— the shared templates, each withname,sizeandmodified.
- Controller:
-
POST /api/v1/admin/templates- Controller:
AdminController::uploadPadTemplate - Auth: admin only
- Params:
name(a.padfile name without a path),content,replace(optional). Sent as a JSON body — URL-encoding a whole file would inflate it several times over. - An existing name is refused with field
template_existsunlessreplaceis set. There is no versioning behind that folder, so overwriting is a decision the caller has to state. - Stores a template offered to everyone in the picker. Rejects anything that is not a usable pad — no frontmatter pad id, a pad on another Etherpad server, empty, or over 2 MiB — because the alternative is an error shown to whoever picks the tile later.
- Controller:
-
POST /api/v1/admin/templates/delete- Controller:
AdminController::deletePadTemplate - Auth: admin only
- Params:
name
- Controller:
-
POST /api/v1/admin/consistency-check- Controller:
AdminController::consistencyCheck - Auth: admin only
- Purpose: optional check of the binding table against the file cache (
docs/architecture.md, "Admin Integrity Check"). - Result:
vanished_file_count: rows whose file the file cache has nothing of, still active and never seen deleted for good, whose pads the app leaves in place;messagesays the check found issues when there are anysamples.vanished_files(file_id,pad_id,access_mode), up to 25- Rows seen deleted for good are not counted: they are on their way, and
pending_delete_countsays how many, as in the health check.
- Controller:
-
POST /api/v1/admin/delete-vanished- Controller:
AdminController::deleteVanished - Auth: admin only
- Params:
fileId - Purpose: the vanished row (see
consistency-check) offileId, while it is still vanished, is marked as a file deleted for good, and the sweep deletes its pad as for any such file - within minutes, or at once throughsettle-pending. Withdelete_pad_with_fileoff it marks nothing, andmessagesays so. When to use it, and when not:docs/deleting-pads.md. - Result:
marked(1or0), and the list as it is after the action, asconsistency-checkgives it (vanished_file_count,samples), withpending_delete_count. 400withInvalid file ID.without a positivefileId.
- Controller:
-
POST /api/v1/admin/delete-all-vanished- Controller:
AdminController::deleteAllVanished - Auth: admin only
- Params:
expected, the count of vanished files the admin was shown and confirmed - Purpose: every vanished row marked as for
delete-vanished, within the budget a sweep has, and never more thanexpected. A list whose count is no longerexpected- grown since it was shown, say - is not taken: nothing is marked, and the answer carries the list as it is now, to be confirmed again. What is left after one call needs another. Up to 500,000 rows the count is that of the rows collected; a longer list is counted by a query of its own. - Result:
marked, and the list as fordelete-vanished. A list not empty after the call is one of two things, andmessagesays which: fewer marked than confirmed, and the rest for another call; or all confirmed marked, and files vanished since, to be looked at first. 400withInvalid count.without a positiveexpected.500when a chunk of marks fails: the marks before it stand,messagesays some may be marked already, and the log has their count.
- Controller:
-
POST /api/v1/admin/forget-vanished- Controller:
AdminController::forgetVanished - Auth: admin only
- Params:
fileId - Purpose: one vanished file's row removed, its public pad left in
Etherpad, while the row is still vanished. The pad's id goes to the
log at
info. A protected pad is not forgotten. Why, and what a forgotten pad is left as:docs/deleting-pads.md. - Result:
forgotten-falsefor a protected pad, withmessagesaying so, or a file no longer vanished - and the list as fordelete-vanished. 400withInvalid file ID.without a positivefileId.
- Controller:
-
POST /api/v1/admin/settle-pending- Controller:
AdminController::settlePending - Auth: admin only
- Purpose: an immediate run of the sweep of files gone for good
(
docs/architecture.md, "Files gone for good"), within the budget its job has, and without waiting out the five minutes a file seen deleted for good waits before its pad goes, nor the hour after Etherpad refused to delete one. It deletes only the pads of files deleted for good, and writes no file. Withdelete_pad_with_fileoff it deletes nothing, andmessagesays so. - Result:
checked: rows of files deleted for good it took;settled: pads it deleted, with their rowspending_delete_count: what is left, named as in the health check
- Controller:
-
viewer_url: URL for viewer redirect. -
embed_url: URL for the minimal authenticated embed page (/embed/by-id/{fileId}). -
pad_id: Etherpad pad ID. -
pad_url: preferred target URL for public/external pads. -
access_mode:publicorprotected. -
status(sync):updatedorunchanged. -
snapshot_rev(sync): Etherpad revision currently persisted in.pad. -
sync_status_url(open/open-by-id): endpoint for revision-based sync status in viewer. -
code(errors): stable identifier on selected error responses. Branch on this, never onmessage— messages are written for people and are translated, the reason a pad on another server could not be linked or read included. The full set:missing_binding(MissingBindingException) — the viewer and embed swap the dead-end error for the recovery UI (POST /api/v1/pads/recover-from-snapshot/{fileId}+ optionalGET /api/v1/pads/find-original/{fileId}lookup).pad_missing(PadLostException) — the file's row names a pad Etherpad has lost: it has none under that id (a protected pad, or a public one whose file holds saved content), or one with no revision and other text than the file saved (a public pad Etherpad made anew, with its default text, when someone visited its address). Only an open that may write asks, once per open; a sync answers it too, for a pad made anew, rather than write that pad over the file. The viewer and embed show the recovery UI without the original-file lookup;POST /api/v1/pads/recover-from-snapshot/{fileId}makes a new pad from the file's content and moves the row onto it.missing_frontmatter(MissingFrontmatterException) — the file has no pad metadata yet; clients callPOST /api/v1/pads/initialize-by-id/{fileId}once and retry the open. A file whose content is neither metadata nor a legacy shortcut cannot be initialised and is refused without this code.pad_too_large(EtherpadTooLargeException) — the pad is past the 5 MiB preview ceiling; it stays editable in Etherpad.pad_file_changed(PadFileChangedException) — the file changed while its pad was being created or initialised; try again. On create, a file may now exist under that name, and the retry says so.pad_type_disabled(PadTypeDisabledException) —403; carriesaccess_modenaming the disabled type, absent when neither type is enabled. See the pad-type settings section.legacy_collision_no_access(LegacyPadCollisionException) — see the legacy migration section.legacy_protected_import_disabled(LegacyProtectedImportDisabledException) —403; the legacy.padnames a group pad and this instance does not import those. The file is left untouched, so the same open succeeds once an admin switches the import back on. See the legacy migration section.
The answers of a public share (
/api/v1/public/...) carry the code that can come up there -pad_too_large- but notmissing_binding,pad_missingormissing_frontmatter: what a client does on those needs a signed-in user. Their messages are translated, one sentence for each kind of trouble.A response without a
codemay still be machine-readable through its HTTP status and other documented fields — a locked.padanswers503withretryable: true, signed in and public alike, for instance, and so does a request this instance's Etherpad could not be reached for. Every error this app answers is JSON, it never answers502or504, and every503of its own carriesretryable: true; what the clients make of a 5xx that is none of these is indocs/architecture.md("Errors of the API"). A file's row that another request made first (BindingNotCreatedException, two initialisations at once, say) answers400withretryable: truetoo: the next open finds that request's pad. So does a pad on another server that did not answer, whose sentence says to try again later; any other reason a pad on another server could not be read answers400without it. The create endpoints answer it with their own sentence and neither field. One Etherpad answered and refused answers400without it: trying again gives the same answer. What is never a stable identifier is themessagetext.
- Controllers use explicit
Set-Cookieresponse headers for Etherpad session bootstrap. - Rationale: this flow needs explicit cookie attributes for iframe cross-subdomain sessions.
- Current contract:
- one custom Etherpad
Set-Cookieheader line is written by this app on protected-open responses that open writable Etherpad iframes - public read-only protected shares render the stored
.padsnapshot and do not set an Etherpad session cookie - no additional custom app cookies are added in the same response
- one custom Etherpad
- If future changes introduce multiple app-level cookies on these responses, this must be implemented and tested explicitly.
src/viewer-init.js- registers the
.padMIME handler synchronously through@nextcloud/viewerfrom an init script. - hands
src/viewer-main.jsover as the handler's component.
- registers the
src/public-share-main.js- opens
/explicitly for a public single-file.padshare. - hands an existing compatibility link with
pathandfilesto the native Viewer once; ordinary public folder-share navigation stays with Nextcloud Files Sharing.
- opens
src/viewer-main.js- prefers
POST /api/v1/pads/open-by-id(fileId, requesttoken). - falls back to
POST /api/v1/pads/open(file, requesttoken) only withoutfileId. - if open fails with missing frontmatter, calls
POST /api/v1/pads/initialize*and retries open once. - if open fails with
code=missing_binding, renders a recovery card with an optionalGET /api/v1/pads/find-original/{fileId}lookup and aPOST /api/v1/pads/recover-from-snapshot/{fileId}action; withcode=pad_missing, the same card without the lookup. - offers "Try again" where the open may work later (
docs/architecture.md, "Errors of the API"). - uses
POST /api/v1/pads/sync/{fileId}periodically and on unload.
- prefers
src/embed-main.js- powers the minimal
/embed/by-id/{fileId}page. - uses same-origin
POST /api/v1/pads/open-by-id. - if open fails with missing frontmatter, calls
POST /api/v1/pads/initialize-by-id/{fileId}and retries once. - if open fails with
code=missing_bindingorpad_missing, renders the same recovery flow as the inline viewer. - offers "Try again" on its error panel where the open may work later, as the viewer does.
- sets the returned
response.urldirectly on the internal iframe. - uses the returned
sync_url/sync_interval_secondsto trigger the same snapshot sync contract as the native viewer. - listens for trusted parent-frame
postMessageevents:epnc:host-visibleepnc:host-hiddenepnc:host-before-closeepnc:host-sync-now
- powers the minimal
src/embed-create-main.js- powers the minimal
/embed/create-by-parent/{parentFolderId}page. - uses same-origin
POST /api/v1/pads/create-by-parent, without a time limit, as it writes. - redirects to returned
embed_urlafter successful pad creation. - tells the host
epnc:create-succeededorepnc:create-failed; afterreason: 'network'the outcome is not known (docs/architecture.md, the create flow).
- powers the minimal
- Normal start:
/index.php/apps/files/files .padopen target:/index.php/apps/files/files/{fileId}?dir=...&editing=false&openfile=true- Legacy/compat fallback deep-link:
/index.php/apps/etherpad_nextcloud/by-id/{fileId}
tests/integration/e2e-pad-flow.sh- happy path: create -> open -> open again
tests/integration/e2e-sync-failure.sh- failure path: create -> sync(force=1) must fail with non-2xx when Etherpad is down
- goal: no silent best-effort success on critical sync
tests/integration/e2e-public-share-folder.sh- folder share: viewer/open/download/reopen + DAV-style
fileparameter + route switch
- folder share: viewer/open/download/reopen + DAV-style
tests/integration/e2e-public-share-single-file.sh- single-file share: viewer/open/download/reopen + DAV-style
fileparameter + route switch
- single-file share: viewer/open/download/reopen + DAV-style
Registered in lib/AppInfo/Application.php.
OCP\EventDispatcher\GenericEvent(Files FullTextSearch's legacy events) ->FullTextSearchIndexingListener,FullTextSearchResultListenerOCP\Security\CSP\AddContentSecurityPolicyEvent->CSPListenerOCA\Files\Event\LoadAdditionalScriptsEvent->LoadFilesScriptsListenerOCA\Files_Sharing\Event\BeforeTemplateRenderedEvent->LoadPublicShareScriptsListenerOCP\Files\Template\RegisterTemplateCreatorEvent->RegisterTemplateCreatorListenerOCP\Files\Template\BeforeGetTemplatesEvent->BeforeGetTemplatesListener(priority -100)OCP\Files\Template\FileCreatedFromTemplateEvent->FileCreatedFromTemplateListenerOCA\Viewer\Event\LoadViewer->LoadViewerListenerOCP\User\Events\UserLoggedOutEvent->UserLoggedOutListenerOCP\User\Events\BeforeUserDeletedEvent->RevokeSessionsOnAccountDeleteListener(the account's sessions, as on a logout)OCA\Files_Trashbin\Events\NodeRestoredEvent->RestoreFromTrashListener- legacy hook
\OCA\Files_Trashbin\Trashbin::post_restore->TrashbinHookHandler::postRestore->RestoreFromTrashListener::handleLegacyHook OCP\Files\Cache\CacheEntryRemovedEvent,OCP\Files\Cache\CacheEntryInsertedEvent,OCP\Files\Cache\CacheEntriesRemovedEvent(from 34, priority 100),OCP\Files\Events\Node\BeforeNodeDeletedEvent,OCP\Files\Events\Node\NodeDeletedEvent,OCP\Files\Events\NodeRemovedFromCache(whatocc files:scandrops), the event named\OCP\Files::postDelete(anOCP\EventDispatcher\GenericEvent),OCA\Files_Trashbin\Events\BeforeNodeRestoredEvent,OCA\Files_Trashbin\Events\NodeRestoredEvent,OCP\User\Events\BeforeUserDeletedEvent,OCP\User\Events\UserDeletedEvent->GoneFilesListener(the marks of files deleted for good;docs/architecture.md, "Files gone for good")OCP\Files\Events\Node\BeforeNodeDeletedEvent,OCP\Files\Events\Node\NodeDeletedEvent->RevokeSessionsOnDeleteListener(the sessions of the protected pads a delete takes along;docs/architecture.md, "Trash/Restore")
etherpad_hostetherpad_api_keyetherpad_api_version(default1.2.15)etherpad_cookie_domain- Optional explicit cookie domain for protected pad session bootstrap.
- Fallback when empty:
- derived from
etherpad_host - IP/invalid hosts -> empty domain attribute
- recommendation: set explicitly for complex proxy/subdomain setups
- derived from
delete_pad_with_file(yes|no, defaultyes) — whether the pad of a.padfile deleted for good is deleted (docs/deleting-pads.md);delete_on_trashbefore, taken over on upgradesync_interval_seconds(default120, clamp5..3600)allow_external_pads(yes|no, defaultno)allow_legacy_protected_import(yes|no, defaultno) — whether a legacy Ownpad.padnaming a group pad may bring that pad in. Turn on only where this Nextcloud is the only thing creating group pads on the Etherpad server; see the legacy migration doc.external_pad_allowlist(newline-separated host list, optional)trusted_embed_origins(newline-separated absolutehttps://originlist, optional)- used for the route-specific
frame-ancestorspolicy on:/embed/by-id/{fileId}/embed/create-by-parent/{parentFolderId}
- when empty, no external embedding origin is added beyond
'self'
- used for the route-specific