Skip to content

Commit e0ccdb4

Browse files
JohnMcLearCopilot
andauthored
Add creator-owned pad settings defaults (#7545)
* Add creator-owned pad settings defaults Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refine pad settings layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix settings popup heading and width Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Explain enforced user settings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Cover creator override flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Let creators bypass enforced settings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address pad settings follow-ups Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2f0b5b0 commit e0ccdb4

19 files changed

Lines changed: 771 additions & 217 deletions

File tree

doc/cookies.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Cookies used by Etherpad.
55
| Name | Sample value | Domain | Path | Expires/max-age | Http-only | Secure | Usage description |
66
|-------------------|----------------------------------|-------------|------|-----------------|-----------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
77
| express_sid | s%3A7yCNjRmTW8ylGQ53I2IhOwYF9... | example.org | / | Session | true | true | Session ID of the [Express web framework](https://expressjs.com). When Etherpad is behind a reverse proxy, and an administrator wants to use session stickiness, he may use this cookie. If you are behind a reverse proxy, please remember to set `trustProxy: true` in `settings.json`. Set in [webaccess.js#L131](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/node/hooks/express/webaccess.js#L131). |
8-
| language | en | example.org | / | Session | false | true | The language of the UI (e.g.: `en-GB`, `it`). Set in [pad_editor.js#L111](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad_editor.js#L111). |
8+
| language | en | example.org | / | Session | false | true | The language of the UI (e.g.: `en-GB`, `it`). Set by the pad client when the user changes **My View → Language** (currently in `src/static/js/pad.ts`, via `setMyViewLanguage()`). |
99
| prefs / prefsHttp | %7B%22epThemesExtTheme%22... | example.org | /p | year 3000 | false | true | Client-side preferences (e.g.: font family, chat always visible, show authorship colors, ...). Set in [pad_cookie.js#L49](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad_cookie.js#L49). `prefs` is used if Etherpad is accessed over HTTPS, `prefsHttp` if accessed over HTTP. For more info see https://github.com/ether/etherpad-lite/issues/3179. |
1010
| token | t.tFzkihhhBf4xKEpCK3PU | example.org | / | 60 days | false | true | A random token representing the author, of the form `t.randomstring_of_lenght_20`. The random string is generated by the client, at ([pad.js#L55-L66](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad.js#L55-L66)). This cookie is always set by the client (at [pad.js#L153-L158](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad.js#L153-L158)) without any solicitation from the server. It is used for all the pads accessed via the web UI (not used for the HTTP API). On the server side, its value is accessed at [SecurityManager.js#L33](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/node/db/SecurityManager.js#L33). |
1111

settings.json.docker

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,12 @@
205205
**/
206206
"enableDarkMode": "${ENABLE_DARK_MODE:true}",
207207

208+
/**
209+
* Enable creator-owned Pad-wide Settings and new-pad default seeding from My View.
210+
* Disabled by default to preserve the legacy single-settings behavior.
211+
**/
212+
"enablePadWideSettings": "${ENABLE_PAD_WIDE_SETTINGS:false}",
213+
208214
/*
209215
* Node native SSL support
210216
*

settings.json.template

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,12 @@
643643
**/
644644
"enableDarkMode": "${ENABLE_DARK_MODE:true}",
645645

646+
/**
647+
* Enable creator-owned Pad-wide Settings and new-pad default seeding from My View.
648+
* Disabled by default to preserve the legacy single-settings behavior.
649+
**/
650+
"enablePadWideSettings": "${ENABLE_PAD_WIDE_SETTINGS:false}",
651+
646652
/*
647653
* From Etherpad 1.8.5 onwards, when Etherpad is in production mode commits from individual users are rate limited
648654
*

src/locales/en.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,19 @@
8383
"pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. This may be due to Etherpad being included in an iFrame in some Browsers. Please ensure Etherpad is on the same subdomain/domain as the parent iFrame",
8484
"pad.permissionDenied": "You do not have permission to access this pad",
8585

86-
"pad.settings.padSettings": "Pad Settings",
86+
"pad.settings.title": "Settings",
87+
"pad.settings.padSettings": "Pad-wide Settings",
88+
"pad.settings.userSettings": "User Settings",
8789
"pad.settings.myView": "My View",
90+
"pad.settings.disablechat": "Disable Chat",
91+
"pad.settings.darkMode": "Dark mode",
8892
"pad.settings.stickychat": "Chat always on screen",
8993
"pad.settings.chatandusers": "Show Chat and Users",
9094
"pad.settings.colorcheck": "Authorship colors",
9195
"pad.settings.linenocheck": "Line numbers",
9296
"pad.settings.rtlcheck": "Read content from right to left?",
97+
"pad.settings.enforceSettings": "Enforce settings for other users",
98+
"pad.settings.enforcedNotice": "These settings are locked for you by this pad's creator. Ask the pad creator if you need them changed.",
9399
"pad.settings.fontType": "Font type:",
94100
"pad.settings.fontType.normal": "Normal",
95101
"pad.settings.language": "Language:",

src/node/db/Pad.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ import pad_utils from "../../static/js/pad_utils";
2727
import {SmartOpAssembler} from "../../static/js/SmartOpAssembler";
2828
import {timesLimit} from "async";
2929

30+
type PadViewSettings = {
31+
showAuthorColors: boolean;
32+
showLineNumbers: boolean;
33+
rtlIsTrue: boolean;
34+
padFontFamily: string;
35+
};
36+
37+
type PadSettings = {
38+
enforceSettings: boolean;
39+
showChat: boolean;
40+
alwaysShowChat: boolean;
41+
chatAndUsers: boolean;
42+
lang: string;
43+
view: PadViewSettings;
44+
};
45+
3046
/**
3147
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
3248
* line breaks and convert Tabs to spaces
@@ -47,6 +63,7 @@ class Pad {
4763
private publicStatus: boolean;
4864
private id: string;
4965
private savedRevisions: any[];
66+
private padSettings: PadSettings;
5067
/**
5168
* @param id
5269
* @param [database] - Database object to access this pad's records (and only this pad's records;
@@ -64,6 +81,26 @@ class Pad {
6481
this.publicStatus = false;
6582
this.id = id;
6683
this.savedRevisions = [];
84+
this.padSettings = Pad.normalizePadSettings();
85+
}
86+
87+
static normalizePadSettings(rawPadSettings: any = {}): PadSettings {
88+
const rawView = rawPadSettings.view ?? {};
89+
return {
90+
enforceSettings: !!rawPadSettings.enforceSettings,
91+
showChat: rawPadSettings.showChat == null ? settings.padOptions.showChat !== false :
92+
!!rawPadSettings.showChat,
93+
alwaysShowChat: !!rawPadSettings.alwaysShowChat,
94+
chatAndUsers: !!rawPadSettings.chatAndUsers,
95+
lang: typeof rawPadSettings.lang === 'string' ? rawPadSettings.lang : 'en',
96+
view: {
97+
showAuthorColors: rawView.showAuthorColors == null ? true : !!rawView.showAuthorColors,
98+
showLineNumbers: rawView.showLineNumbers == null ?
99+
settings.padOptions.showLineNumbers !== false : !!rawView.showLineNumbers,
100+
rtlIsTrue: !!rawView.rtlIsTrue,
101+
padFontFamily: typeof rawView.padFontFamily === 'string' ? rawView.padFontFamily : '',
102+
},
103+
};
67104
}
68105

69106
apool() {
@@ -88,6 +125,22 @@ class Pad {
88125
return this.publicStatus;
89126
}
90127

128+
getPadSettings() {
129+
return Pad.normalizePadSettings(this.padSettings);
130+
}
131+
132+
setPadSettings(rawPadSettings: any) {
133+
const nextPadSettings = {
134+
...this.getPadSettings(),
135+
...rawPadSettings,
136+
view: {
137+
...this.getPadSettings().view,
138+
...(rawPadSettings?.view ?? {}),
139+
},
140+
};
141+
this.padSettings = Pad.normalizePadSettings(nextPadSettings);
142+
}
143+
91144
/**
92145
* Appends a new revision
93146
* @param {Object} aChangeset The changeset to append to the pad
@@ -400,6 +453,7 @@ class Pad {
400453
const firstChangeset = makeSplice('\n', 0, 0, text, firstAttribs, this.pool);
401454
await this.appendRevision(firstChangeset, authorId);
402455
}
456+
this.padSettings = Pad.normalizePadSettings(this.padSettings);
403457
await hooks.aCallAll('padLoad', {pad: this});
404458
}
405459

src/node/handler/PadMessageHandler.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import {RateLimiterMemory} from 'rate-limiter-flexible';
4646
import {ChangesetRequest, PadUserInfo, SocketClientRequest} from "../types/SocketClientRequest";
4747
import {APool, AText, PadAuthor, PadType} from "../types/PadType";
4848
import {ChangeSet} from "../types/ChangeSet";
49-
import {ChatMessageMessage, ClientReadyMessage, ClientSaveRevisionMessage, ClientSuggestUserName, ClientUserChangesMessage, ClientVarMessage, CustomMessage, PadDeleteMessage, UserNewInfoMessage} from "../../static/js/types/SocketIOMessage";
49+
import {ChatMessageMessage, ClientReadyMessage, ClientSaveRevisionMessage, ClientSuggestUserName, ClientUserChangesMessage, ClientVarMessage, CustomMessage, PadDeleteMessage, PadOptionsMessage, UserNewInfoMessage} from "../../static/js/types/SocketIOMessage";
5050
import {Builder} from "../../static/js/Builder";
5151
const webaccess = require('../hooks/express/webaccess');
5252
const { checkValidRev } = require('../utils/checkValidRev');
@@ -263,6 +263,41 @@ const handlePadDelete = async (socket: any, padDeleteMessage: PadDeleteMessage)
263263
}
264264
}
265265

266+
const isPadCreator = async (pad: any, authorId: string) => authorId === await pad.getRevisionAuthor(0);
267+
268+
const handlePadOptionsMessage = async (
269+
socket: any, message: PadOptionsMessage & {data: {payload: PadOptionsMessage}}) => {
270+
const session = sessioninfos[socket.id];
271+
if (!session || !session.author || !session.padId) throw new Error('session not ready');
272+
if (!settings.enablePadWideSettings) return;
273+
if (!await padManager.doesPadExist(session.padId)) {
274+
messageLogger.warn(`Ignoring padoptions for missing pad ${session.padId}`);
275+
return;
276+
}
277+
const pad = await padManager.getPad(session.padId, null, session.author);
278+
if (!await isPadCreator(pad, session.author)) {
279+
socket.emit('shout', {
280+
type: 'COLLABROOM',
281+
data: {
282+
type: 'shoutMessage',
283+
payload: {
284+
message: {
285+
message: 'Only the pad creator can change pad settings',
286+
sticky: false,
287+
},
288+
timestamp: Date.now(),
289+
},
290+
},
291+
});
292+
return;
293+
}
294+
pad.setPadSettings(message.data.payload.options);
295+
await pad.saveToDatabase();
296+
_getRoomSockets(session.padId).forEach((socket) => {
297+
socket.emit('message', message);
298+
});
299+
};
300+
266301

267302
/**
268303
* Handles a message from a user
@@ -413,6 +448,11 @@ exports.handleMessage = async (socket:any, message: ClientVarMessage) => {
413448
try {
414449
switch (type) {
415450
case 'suggestUserName': handleSuggestUserName(socket, message as unknown as ClientSuggestUserName); break;
451+
case 'padoptions':
452+
await handlePadOptionsMessage(
453+
socket,
454+
message as unknown as PadOptionsMessage & {data: {payload: PadOptionsMessage}});
455+
break;
416456
default: throw new Error('unknown message type');
417457
}
418458
} catch (err) {
@@ -883,8 +923,13 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
883923
]);
884924
({colorId: authorColorId, name: authorName} = await authorManager.getAuthor(sessionInfo.author));
885925

926+
const padExisted = await padManager.doesPadExist(sessionInfo.padId);
886927
// load the pad-object from the database
887928
const pad = await padManager.getPad(sessionInfo.padId, null, sessionInfo.author);
929+
if (settings.enablePadWideSettings && !padExisted && message.padSettingsDefaults) {
930+
pad.setPadSettings(message.padSettingsDefaults);
931+
await pad.saveToDatabase();
932+
}
888933

889934
// these db requests all need the pad object (timestamp of latest revision, author data)
890935
const authors = pad.getAllAuthors();
@@ -1025,6 +1070,8 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
10251070

10261071
// Warning: never ever send sessionInfo.padId to the client. If the client is read only you
10271072
// would open a security hole 1 swedish mile wide...
1073+
const canEditPadSettings = settings.enablePadWideSettings &&
1074+
!sessionInfo.readonly && await isPadCreator(pad, sessionInfo.author);
10281075
const clientVars:MapArrayType<any> = {
10291076
skinName: settings.skinName,
10301077
skinVariants: settings.skinVariants,
@@ -1033,9 +1080,10 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
10331080
maxRevisions: 100,
10341081
},
10351082
enableDarkMode: settings.enableDarkMode,
1083+
enablePadWideSettings: settings.enablePadWideSettings,
10361084
automaticReconnectionTimeout: settings.automaticReconnectionTimeout,
10371085
initialRevisionList: [],
1038-
initialOptions: {},
1086+
initialOptions: pad.getPadSettings(),
10391087
savedRevisions: pad.getSavedRevisions(),
10401088
collab_client_vars: {
10411089
initialAttributedText: atext,
@@ -1060,6 +1108,7 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
10601108
numConnectedUsers: roomSockets.length + 1, // +1 for this user (not yet in room)
10611109
readOnlyId: sessionInfo.readOnlyPadId,
10621110
readonly: sessionInfo.readonly,
1111+
canEditPadSettings,
10631112
serverTimestamp: Date.now(),
10641113
sessionRefreshInterval: settings.cookie.sessionRefreshInterval,
10651114
userId: sessionInfo.author,

src/node/utils/Settings.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export type SettingsType = {
172172
},
173173
updateServer: string,
174174
enableDarkMode: boolean,
175+
enablePadWideSettings: boolean,
175176
skinName: string | null,
176177
skinVariants: string,
177178
ip: string,
@@ -294,7 +295,7 @@ export type SettingsType = {
294295
lowerCasePadIds: boolean,
295296
randomVersionString: string,
296297
gitVersion: string
297-
getPublicSettings: () => Pick<SettingsType, "title" | "skinVariants"|"randomVersionString"|"skinName"|"toolbar"| "exposeVersion"| "gitVersion">,
298+
getPublicSettings: () => Pick<SettingsType, "title" | "skinVariants"|"randomVersionString"|"skinName"|"toolbar"| "exposeVersion"| "gitVersion" | "enablePadWideSettings">,
298299
}
299300

300301
const settings: SettingsType = {
@@ -328,6 +329,7 @@ const settings: SettingsType = {
328329
},
329330
updateServer: "https://static.etherpad.org",
330331
enableDarkMode: true,
332+
enablePadWideSettings: false,
331333
/*
332334
* Skin name.
333335
*
@@ -657,6 +659,7 @@ const settings: SettingsType = {
657659
title: settings.title,
658660
skinName: settings.skinName,
659661
skinVariants: settings.skinVariants,
662+
enablePadWideSettings: settings.enablePadWideSettings,
660663
}
661664
},
662665
gitVersion: getGitCommit(),

src/static/js/chat.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ exports.chat = (() => {
3434
let chatMentions = 0;
3535
return {
3636
show() {
37+
if (pad.settings.hideChat) return;
3738
$('#chaticon').removeClass('visible');
3839
$('#chatbox').addClass('visible');
3940
this.scrollDown(true);
@@ -49,7 +50,7 @@ exports.chat = (() => {
4950
}, 100);
5051
},
5152
// Make chat stick to right hand side of screen
52-
stickToScreen(fromInitialCall) {
53+
stickToScreen(fromInitialCall, persistPreference = true) {
5354
if ($('#options-stickychat').prop('checked')) {
5455
$('#options-stickychat').prop('checked', false);
5556
}
@@ -65,13 +66,13 @@ exports.chat = (() => {
6566
$('#chatbox').css('display', 'flex');
6667
}, 0);
6768

68-
padcookie.setPref('chatAlwaysVisible', isStuck);
69+
if (persistPreference) padcookie.setPref('chatAlwaysVisible', isStuck);
6970
$('#options-stickychat').prop('checked', isStuck);
7071
},
71-
chatAndUsers(fromInitialCall) {
72+
chatAndUsers(fromInitialCall, persistPreference = true) {
7273
const toEnable = $('#options-chatandusers').is(':checked');
7374
if (toEnable || !userAndChat || fromInitialCall) {
74-
this.stickToScreen(true);
75+
this.stickToScreen(true, persistPreference);
7576
$('#options-stickychat').prop('checked', true);
7677
$('#options-chatandusers').prop('checked', true);
7778
$('#options-stickychat').prop('disabled', true);
@@ -80,7 +81,7 @@ exports.chat = (() => {
8081
$('#options-stickychat').prop('disabled', false);
8182
userAndChat = false;
8283
}
83-
padcookie.setPref('chatAndUsers', userAndChat);
84+
if (persistPreference) padcookie.setPref('chatAndUsers', userAndChat);
8485
$('#users, .sticky-container')
8586
.toggleClass('chatAndUsers popup-show stickyUsers', userAndChat);
8687
$('#chatbox').toggleClass('chatAndUsersChat', userAndChat);
@@ -204,7 +205,7 @@ exports.chat = (() => {
204205
count++;
205206
$('#chatcounter').text(count);
206207

207-
if (!chatOpen && ctx.duration > 0) {
208+
if (!pad.settings.hideChat && !chatOpen && ctx.duration > 0) {
208209
const text = $('<p>')
209210
.append($('<span>').addClass('author-name').text(ctx.authorName))
210211
// ctx.text was HTML-escaped before calling the hook. Hook functions are trusted

0 commit comments

Comments
 (0)