Skip to content
Merged
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,
Comment thread
JohnMcLear marked this conversation as resolved.
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 thread
JohnMcLear marked this conversation as resolved.
<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 @@ -432,6 +432,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