Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions settings.json.docker
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,12 @@

/**
* Enable creator-owned Pad-wide Settings and new-pad default seeding from My View.
* Disabled by default to preserve the legacy single-settings behavior.
* The pad creator (revision-0 author) gets the "Pad-wide Settings" section,
* which lets them set defaults and optionally enforce them for other users.
* Other users see only "User Settings" (their own view options).
* Set to false to revert to the legacy single-settings behavior.
**/
"enablePadWideSettings": "${ENABLE_PAD_WIDE_SETTINGS:false}",
"enablePadWideSettings": "${ENABLE_PAD_WIDE_SETTINGS:true}",

/*
* Optional privacy banner. See settings.json.template for full field docs.
Expand Down
7 changes: 5 additions & 2 deletions settings.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -733,9 +733,12 @@

/**
* Enable creator-owned Pad-wide Settings and new-pad default seeding from My View.
* Disabled by default to preserve the legacy single-settings behavior.
* The pad creator (revision-0 author) gets the "Pad-wide Settings" section,
* which lets them set defaults and optionally enforce them for other users.
* Other users see only "User Settings" (their own view options).
* Set to false to revert to the legacy single-settings behavior.
**/
"enablePadWideSettings": "${ENABLE_PAD_WIDE_SETTINGS:false}",
"enablePadWideSettings": "${ENABLE_PAD_WIDE_SETTINGS:true}",

/*
* Optional privacy banner shown once the pad loads. Disabled by default.
Expand Down
2 changes: 1 addition & 1 deletion src/node/utils/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ const settings: SettingsType = {
},
updateServer: "https://static.etherpad.org",
enableDarkMode: true,
enablePadWideSettings: false,
enablePadWideSettings: true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. enablepadwidesettings default enabled 📘 Rule violation ☼ Reliability

The PR flips enablePadWideSettings to default to true, enabling the feature without explicit
operator opt-in. This violates the requirement that new/flagged functionality be disabled by default
and can also change behavior for deployments with older configs that omit this key.
Agent Prompt
## Issue description
`enablePadWideSettings` is now enabled by default (`true`) in runtime defaults and shipped config templates, which violates the requirement that feature-flagged functionality be disabled by default and may change behavior for installs that do not set this config key.

## Issue Context
The PR flips defaults in:
- runtime defaults (`src/node/utils/Settings.ts`)
- `settings.json.template`
- `settings.json.docker`

## Fix Focus Areas
- src/node/utils/Settings.ts[370-373]
- settings.json.template[733-741]
- settings.json.docker[247-255]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Intentional. The maintainer (@JohnMcLear) explicitly approved flipping this default in conversation prior to opening the PR — see the PR description under "Why this is a behavior change worth taking." The creator-only canEditPadSettings server gate is unchanged, so this widens UI defaults but does not widen access. Operators who want the legacy single-section modal can still set enablePadWideSettings: false; the comment in both shipped settings files documents that path.

allowPadDeletionByAllUsers: false,
privacyBanner: {
enabled: false,
Expand Down
4 changes: 0 additions & 4 deletions src/templates/pad.html
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,7 @@
<!------------------------------------------------------------->

<div id="settings" class="popup" role="dialog" aria-modal="true" aria-labelledby="settings-title"><div class="popup-content">
<% if (settings.enablePadWideSettings) { %>
<h1 id="settings-title" data-l10n-id="pad.settings.title">Settings</h1>
<% } else { %>
<h1 id="settings-title" data-l10n-id="pad.settings.padSettings">Pad Settings</h1>
<% } %>
<div class="settings-sections">
Comment on lines 129 to 131
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. No test for modal h1 📘 Rule violation ☼ Reliability

The PR fixes the settings modal heading logic by removing the conditional title, but it does not add
a regression test that would fail if the old misleading heading behavior returned. This makes the UI
bug fix unverifiable and prone to regression.
Agent Prompt
## Issue description
The settings modal heading bug fix (always using `pad.settings.title` / `Settings`) has no automated regression coverage.

## Issue Context
This PR changes the modal `<h1 id="settings-title">` rendering logic in `src/templates/pad.html`, but does not add a test that asserts the heading text/label under the relevant configuration states.

## Fix Focus Areas
- src/templates/pad.html[129-138]
- src/tests/frontend-new/specs/pad_settings.spec.ts[1-40]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 912da32. Added tests/backend/specs/settingsModalHeading.ts which asserts data-l10n-id="pad.settings.title" is rendered for the modal H1 in both enablePadWideSettings states. If the old conditional (pad.settings.padSettings when the flag is off) is reintroduced, the disabled-flag test fails.

<div id="user-settings-section" class="settings-section">
<% e.begin_block("mySettings"); %>
Expand Down
45 changes: 45 additions & 0 deletions src/tests/backend/specs/settingsModalHeading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict';

import {MapArrayType} from '../../../node/types/MapType';
import settings from '../../../node/utils/Settings';

const assert = require('assert').strict;
const common = require('../common');

// Regression coverage for the settings modal title. With
// `enablePadWideSettings: false` the template used to render
// `data-l10n-id="pad.settings.padSettings"` ("Pad-wide Settings") for every
// user, even though no pad-wide controls were rendered in that mode. The fix
// removes the conditional and always uses `pad.settings.title` ("Settings").
describe(__filename, function () {
this.timeout(30000);
let agent: any;
const backup: MapArrayType<any> = {};

before(async function () { agent = await common.init(); });

beforeEach(async function () {
backup.enablePadWideSettings = settings.enablePadWideSettings;
});

afterEach(async function () {
settings.enablePadWideSettings = backup.enablePadWideSettings;
});

const titleH1 = (html: string): string | null => {
const m = html.match(/<h1\s+id="settings-title"[^>]*data-l10n-id="([^"]+)"/);
return m ? m[1] : null;
};

it('uses pad.settings.title with the feature enabled', async function () {
settings.enablePadWideSettings = true;
const res = await agent.get('/p/headingTest').expect(200);
assert.equal(titleH1(res.text), 'pad.settings.title');
});

it('uses pad.settings.title with the feature disabled (no misleading "Pad-wide" label)', async function () {
settings.enablePadWideSettings = false;
const res = await agent.get('/p/headingTest').expect(200);
assert.equal(titleH1(res.text), 'pad.settings.title');
});
});
62 changes: 61 additions & 1 deletion src/tests/backend/specs/socketio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe(__filename, function () {
plugins.hooks[hookName] = [];
}
backups.settings = {};
for (const setting of ['editOnly', 'requireAuthentication', 'requireAuthorization', 'users']) {
for (const setting of ['editOnly', 'requireAuthentication', 'requireAuthorization', 'users', 'enablePadWideSettings']) {
// @ts-ignore
backups.settings[setting] = settings[setting];
}
Expand Down Expand Up @@ -324,6 +324,66 @@ describe(__filename, function () {
});
});

describe('Pad-wide settings creator gate', function () {
let socketA: any;
let socketB: any;

const removeIfExists = async (padId: string) => {
if (await padManager.doesPadExist(padId)) {
const p = await padManager.getPad(padId);
await p.remove();
}
};

beforeEach(async function () {
// @ts-ignore - test toggles a public setting
settings.enablePadWideSettings = true;
await removeIfExists('foo');
});

afterEach(async function () {
for (const s of [socketA, socketB]) if (s) s.close();
socketA = null;
socketB = null;
socket = null;
await removeIfExists('foo');
});

it('different browsers (separate cookie jars): only the creator gets canEditPadSettings', async function () {
const supertest = require('supertest');
const browserA = supertest(common.baseUrl);
const browserB = supertest(common.baseUrl);

const resA = await browserA.get('/p/foo').expect(200);
socketA = await common.connect(resA);
const cvA = await common.handshake(socketA, 'foo');
assert.equal(cvA.data.canEditPadSettings, true,
'first joiner (creator) should see Pad-wide Settings');

const resB = await browserB.get('/p/foo').expect(200);
socketB = await common.connect(resB);
const cvB = await common.handshake(socketB, 'foo');
assert.equal(cvB.data.canEditPadSettings, false,
'non-creator joiner must NOT see Pad-wide Settings');
});

it('same browser two tabs (shared cookie jar): BOTH get canEditPadSettings=true', async function () {
// Reusing the same response (and its set-cookie header) for both
// connects is the backend equivalent of two browser tabs sharing the
// same HttpOnly token cookie — same authorID, same creator.
const res = await agent.get('/p/foo').expect(200);

socketA = await common.connect(res);
const cvA = await common.handshake(socketA, 'foo');
assert.equal(cvA.data.canEditPadSettings, true);

socketB = await common.connect(res);
const cvB = await common.handshake(socketB, 'foo');
assert.equal(cvB.data.canEditPadSettings, true,
'same author across tabs is one identity, both are the creator');
});
});

describe('SocketIORouter.js', function () {
const Module = class {
setSocketIO(io:any) {}
Expand Down
Loading