Skip to content

Commit 04ef397

Browse files
authored
feat(admin): let instances choose which pad types are offered (#177)
* feat(admin): let instances choose which pad types are offered Add two settings, Protected pads and Public pads, both enabled by default so existing installations are unaffected. The labels keep the vocabulary the API and the binding table already use. Enforcement is server-side, matching allow_external_pads: PadCreationService covers the API and embed-create paths, materializeTemplateInto() the templates, and PadBootstrapService guards its `binding === null` branch -- otherwise an empty .pad plus /initialize was a way around the setting. The check deliberately stays out of provisionPadId(), which also re-initialises files that already have a binding. Existing pads of a disabled type keep working; only creation is affected. Turning both off is allowed. A template carrying a disabled mode is created in the enabled mode rather than failing, and Nextcloud's own "+ New pad" entry is hidden when protected pads are off, since it always produces one. External pads stay independent. The setting decides what the app offers -- it is not a hard boundary against hand-crafted .pad files. Recovering an orphaned pad from its snapshot still re-provisions the mode recorded in the file, because rescuing existing content wins. Includes de/es/fr translations and doc updates. Closes #175. * fix(api): return a stable code instead of the exception message PadControllerErrorMapper documents that RuntimeException messages are not exposed to clients, but the new PadTypeDisabledException branch forwarded its message verbatim. That coupled the HTTP contract to service-layer prose. Carry the access mode on the exception as data instead, and let the mapper build the payload: a fixed message plus `code: pad_type_disabled` and, when a single type is to blame, `access_mode`. This matches how `missing_binding` and `legacy_collision_no_access` already work, and gives clients something stable to branch on. * fix: fall back instead of stranding empty .pad files initializeMissingFrontmatter hard-required protected pads, so a `.pad` that arrived outside the UI -- over WebDAV, from another integration, or from before the setting changed -- failed every open attempt with 403 when protected pads were off, even though public pads were available. There was no way out for the user: recovery needs frontmatter the file doesn't have. Use the same fallback the template path already uses. It still closes the `/initialize` bypass (nothing enabled means an exception), but produces an openable pad whenever some type is allowed. Rename resolveForTemplate() to resolveCreatableMode(), since it now serves both callers, and note in its docblock that the fallback can widen access: a protected template becomes a public pad when protected pads are off. README, api-reference and templates docs say so too. * fix(admin): actually hide the "no pad type enabled" hint The hint carries Nextcloud's `.settings-hint` class alongside ours, and that rule sets `display`, so it beat the user-agent `[hidden]` default -- the hint showed permanently, even with both pad types enabled. Toggle `display` instead, matching how the external-allowlist rows in the same file already work, and set the initial state from PHP so nothing flashes before the script runs.
1 parent 0751fa0 commit 04ef397

54 files changed

Lines changed: 1020 additions & 33 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,37 @@ and configure:
9595
- Etherpad API URL (optional; defaults to Base URL)
9696
- Etherpad API key (OAuth is not required; Etherpad API key auth is used)
9797
- Copy content to `.pad` file interval
98+
- Which pad types users may create (see below)
9899
- Delete-on-trash policy
99100
- External public pad policy
100101

102+
Two settings control which pad types the app offers, both enabled by default:
103+
104+
- **Protected pads** — only people who can open the `.pad` file in Nextcloud
105+
can open the pad. Created as Etherpad group pads, which require a session
106+
issued by Nextcloud.
107+
- **Public pads** — anyone with the pad link can open the pad, without a
108+
Nextcloud account.
109+
110+
Switching a type off hides its `+ New` entries and refuses new pads of that
111+
type on the API create endpoints. Pads that already exist keep working either
112+
way.
113+
114+
Two paths fall back rather than refuse, so nothing gets stranded: a template
115+
carrying the disabled mode, and the first-open initialisation of a `.pad`
116+
that arrived outside the app (over WebDAV, say). Both create the pad in the
117+
enabled mode; only with both types off is creation refused outright. Note
118+
that this can widen access — a protected template becomes a public pad when
119+
protected pads are off.
120+
121+
The setting decides what the app offers; it is not a hard boundary against a
122+
user who crafts `.pad` files by hand. Recovering an orphaned `.pad` from its
123+
snapshot still re-provisions the mode recorded in that file, because rescuing
124+
existing content takes precedence.
125+
126+
Linking external public pads is governed separately by the external pad
127+
policy and is unaffected by these two settings.
128+
101129
### 5) Check iframe and cookie setup for protected pads
102130

103131
If protected pads should open inside the Nextcloud viewer iframe:

docs/api-reference.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,23 @@ Base: `/apps/etherpad_nextcloud`
7171
- read-only protected share: `is_readonly_snapshot=true`, empty `url`, `snapshot_text`, and sanitized `snapshot_html`; no Etherpad session cookie
7272
- public/external pad share: regular public Etherpad URL
7373

74+
Admins can switch either pad type off (see the admin settings). `POST /pads`
75+
and `POST /pads/create-by-parent` refuse a disabled `accessMode` with `403`.
76+
Pads that already exist are unaffected.
77+
78+
`POST /pads/from-template` and the `initialize` endpoints do not refuse a
79+
disabled mode — they create the pad in the enabled mode instead, so a
80+
template or a `.pad` that arrived outside the UI stays usable. They only
81+
return `403` when no pad type is enabled at all. Note this can widen access:
82+
a protected template becomes a public pad when protected pads are off.
83+
84+
The refusal carries `code: pad_type_disabled`, plus `access_mode` naming the
85+
disabled type — that field is absent when no pad type is enabled at all.
86+
Branch on the code rather than on the message text.
87+
88+
`POST /pads/from-url` is deliberately exempt: external pads are governed
89+
solely by the separate external-pad policy, not by these two settings.
90+
7491
- `POST /api/v1/pads`
7592
- Controller: `PadCreateController::create`
7693
- Params:

docs/architecture.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ Etherpad is the editing source of truth; the `.pad` file acts as binding storage
2929
- Revision metadata and snapshot body structure (`[TEXT]`, `[HTML-BEGIN]`, `[HTML-END]`).
3030
- `lib/Service/PadSessionService.php`
3131
- Session-cookie flow for protected GroupPads.
32+
- `lib/Service/PadTypePolicy.php`
33+
- Which pad types the instance offers (both enabled by default).
34+
- Guards creation only; existing pads of a disabled type keep working.
35+
- Resolves a template's access mode to an enabled one instead of failing.
3236
- `lib/Service/ConsistencyCheckService.php`
3337
- Optional admin integrity scan:
3438
- bindings without file
@@ -86,7 +90,7 @@ checked-in runtime assets in `js/`.
8690

8791
### 1) Create
8892

89-
1. `PadCreateController::create` creates an Etherpad pad (public or protected/group).
93+
1. `PadCreateController::create` creates an Etherpad pad (public or protected/group). `PadCreationService` asks `PadTypePolicy` first and refuses a pad type the admin switched off; the check sits in the create paths rather than in `PadBootstrapService::provisionPadId`, which also serves existing files.
9094
2. Creates the `.pad` file.
9195
3. Writes initial frontmatter.
9296
4. Creates DB binding.
@@ -249,6 +253,7 @@ Primary flow (native viewer when available):
249253
- `+ Neu` integration for `Public pad` with runtime capability checks:
250254
- modern API: `addNewFileMenuEntry` / `getNewFileMenu().registerEntry`
251255
- legacy API fallback: `OC.Plugins.register('OCA.Files.NewFileMenu', ...)`
256+
- The `Public pad` entry follows the admin setting, read from initial state. `Public pad from URL` is always registered; the external-pad policy is enforced server-side when the create call arrives.
252257
- `src/files/public-share-pad-links.js`
253258
- Public-share click interception for download links that need remapping to the pad viewer.
254259
- Authenticated Files routes intentionally do not use global click interception.

docs/etherpad-integration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ Important:
3737
- GroupPad ID (`g.<group>$<name>`).
3838
- Access only with a valid Etherpad session (`sessionID` cookie).
3939

40+
Either type can be switched off in the admin settings, which stops new pads of
41+
that type from being created. Existing pads keep opening regardless, so the
42+
setting never cuts anyone off from content.
43+
4044
## Session Flow (protected)
4145

4246
Implemented in `lib/Service/PadSessionService.php`.

docs/templates.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ Response shape on success (200):
8080

8181
Error responses:
8282
- **400** — template is not a `.pad`, template is external (`ext.*`), template is empty, target path invalid
83+
- **403** — no pad type is enabled in the admin settings, so nothing can be created
8384
- **404** — template file ID does not resolve in the user's userspace
8485
- **409** — target filename collides with an existing file (`A file with this name already exists.`)
8586
- **500** — Etherpad unreachable or other unexpected failure
@@ -90,3 +91,4 @@ Error responses:
9091
- **Failed template materialisation falls back to a blank pad.** If anything in the listener throws (binding race, Etherpad unreachable, malformed template), the byte-copy NC made is wiped and the new file behaves like a normal empty `.pad` — the regular missing-frontmatter init kicks in on first open.
9192
- **Placeholder substitution applies to both the plain-text and the HTML snapshot in the body**. If a placeholder ends up inside an HTML attribute (`<a href="{{date}}">`), it gets resolved too — keep placeholders in human-readable locations to avoid surprises.
9293
- **No template registry** — every `.pad` in the user's *Templates* folder is a candidate. There's no separate "is a template" flag.
94+
- **A template keeps its own access mode**, unless the admin switched that pad type off. In that case the pad is created in the enabled mode instead of failing, so the template's content still lands. With no pad type enabled at all, template creation is refused. Be aware this can widen access: a protected template creates a public pad when protected pads are off — on such an instance that is the only option, but the resulting pad is open to anyone with the link.
Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)