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
2 changes: 1 addition & 1 deletion doc/cookies.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Cookies used by Etherpad.
| Name | Sample value | Domain | Path | Expires/max-age | Http-only | Secure | Usage description |
|-------------------|----------------------------------|-------------|------|-----------------|-----------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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). |
| 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). |
| 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()`). |
| 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. |
| 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). |

Expand Down
6 changes: 6 additions & 0 deletions settings.json.docker
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@
**/
"enableDarkMode": "${ENABLE_DARK_MODE:true}",

/**
* Enable creator-owned Pad-wide Settings and new-pad default seeding from My View.
* Disabled by default to preserve the legacy single-settings behavior.
**/
"enablePadWideSettings": "${ENABLE_PAD_WIDE_SETTINGS:false}",

/*
* Node native SSL support
*
Expand Down
6 changes: 6 additions & 0 deletions settings.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,12 @@
**/
"enableDarkMode": "${ENABLE_DARK_MODE:true}",

/**
* Enable creator-owned Pad-wide Settings and new-pad default seeding from My View.
* Disabled by default to preserve the legacy single-settings behavior.
**/
"enablePadWideSettings": "${ENABLE_PAD_WIDE_SETTINGS:false}",

/*
* From Etherpad 1.8.5 onwards, when Etherpad is in production mode commits from individual users are rate limited
*
Expand Down
8 changes: 7 additions & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,19 @@
"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",
"pad.permissionDenied": "You do not have permission to access this pad",

"pad.settings.padSettings": "Pad Settings",
"pad.settings.title": "Settings",
"pad.settings.padSettings": "Pad-wide Settings",
"pad.settings.userSettings": "User Settings",
"pad.settings.myView": "My View",
"pad.settings.disablechat": "Disable Chat",
"pad.settings.darkMode": "Dark mode",
"pad.settings.stickychat": "Chat always on screen",
"pad.settings.chatandusers": "Show Chat and Users",
"pad.settings.colorcheck": "Authorship colors",
"pad.settings.linenocheck": "Line numbers",
"pad.settings.rtlcheck": "Read content from right to left?",
"pad.settings.enforceSettings": "Enforce settings for other users",
"pad.settings.enforcedNotice": "These settings are locked for you by this pad's creator. Ask the pad creator if you need them changed.",
"pad.settings.fontType": "Font type:",
"pad.settings.fontType.normal": "Normal",
"pad.settings.language": "Language:",
Expand Down
54 changes: 54 additions & 0 deletions src/node/db/Pad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ import pad_utils from "../../static/js/pad_utils";
import {SmartOpAssembler} from "../../static/js/SmartOpAssembler";
import {timesLimit} from "async";

type PadViewSettings = {
showAuthorColors: boolean;
showLineNumbers: boolean;
rtlIsTrue: boolean;
padFontFamily: string;
};

type PadSettings = {
enforceSettings: boolean;
showChat: boolean;
alwaysShowChat: boolean;
chatAndUsers: boolean;
lang: string;
view: PadViewSettings;
};

/**
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
* line breaks and convert Tabs to spaces
Expand All @@ -47,6 +63,7 @@ class Pad {
private publicStatus: boolean;
private id: string;
private savedRevisions: any[];
private padSettings: PadSettings;
/**
* @param id
* @param [database] - Database object to access this pad's records (and only this pad's records;
Expand All @@ -64,6 +81,26 @@ class Pad {
this.publicStatus = false;
this.id = id;
this.savedRevisions = [];
this.padSettings = Pad.normalizePadSettings();
}

static normalizePadSettings(rawPadSettings: any = {}): PadSettings {
const rawView = rawPadSettings.view ?? {};
return {
enforceSettings: !!rawPadSettings.enforceSettings,
showChat: rawPadSettings.showChat == null ? settings.padOptions.showChat !== false :
!!rawPadSettings.showChat,
alwaysShowChat: !!rawPadSettings.alwaysShowChat,
chatAndUsers: !!rawPadSettings.chatAndUsers,
lang: typeof rawPadSettings.lang === 'string' ? rawPadSettings.lang : 'en',
view: {
showAuthorColors: rawView.showAuthorColors == null ? true : !!rawView.showAuthorColors,
showLineNumbers: rawView.showLineNumbers == null ?
settings.padOptions.showLineNumbers !== false : !!rawView.showLineNumbers,
rtlIsTrue: !!rawView.rtlIsTrue,
padFontFamily: typeof rawView.padFontFamily === 'string' ? rawView.padFontFamily : '',
},
};
}

apool() {
Expand All @@ -88,6 +125,22 @@ class Pad {
return this.publicStatus;
}

getPadSettings() {
return Pad.normalizePadSettings(this.padSettings);
}

setPadSettings(rawPadSettings: any) {
const nextPadSettings = {
...this.getPadSettings(),
...rawPadSettings,
view: {
...this.getPadSettings().view,
...(rawPadSettings?.view ?? {}),
},
};
this.padSettings = Pad.normalizePadSettings(nextPadSettings);
}

/**
* Appends a new revision
* @param {Object} aChangeset The changeset to append to the pad
Expand Down Expand Up @@ -400,6 +453,7 @@ class Pad {
const firstChangeset = makeSplice('\n', 0, 0, text, firstAttribs, this.pool);
await this.appendRevision(firstChangeset, authorId);
}
this.padSettings = Pad.normalizePadSettings(this.padSettings);
await hooks.aCallAll('padLoad', {pad: this});
}

Expand Down
53 changes: 51 additions & 2 deletions src/node/handler/PadMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {RateLimiterMemory} from 'rate-limiter-flexible';
import {ChangesetRequest, PadUserInfo, SocketClientRequest} from "../types/SocketClientRequest";
import {APool, AText, PadAuthor, PadType} from "../types/PadType";
import {ChangeSet} from "../types/ChangeSet";
import {ChatMessageMessage, ClientReadyMessage, ClientSaveRevisionMessage, ClientSuggestUserName, ClientUserChangesMessage, ClientVarMessage, CustomMessage, PadDeleteMessage, UserNewInfoMessage} from "../../static/js/types/SocketIOMessage";
import {ChatMessageMessage, ClientReadyMessage, ClientSaveRevisionMessage, ClientSuggestUserName, ClientUserChangesMessage, ClientVarMessage, CustomMessage, PadDeleteMessage, PadOptionsMessage, UserNewInfoMessage} from "../../static/js/types/SocketIOMessage";
import {Builder} from "../../static/js/Builder";
const webaccess = require('../hooks/express/webaccess');
const { checkValidRev } = require('../utils/checkValidRev');
Expand Down Expand Up @@ -263,6 +263,41 @@ const handlePadDelete = async (socket: any, padDeleteMessage: PadDeleteMessage)
}
}

const isPadCreator = async (pad: any, authorId: string) => authorId === await pad.getRevisionAuthor(0);

const handlePadOptionsMessage = async (
socket: any, message: PadOptionsMessage & {data: {payload: PadOptionsMessage}}) => {
const session = sessioninfos[socket.id];
if (!session || !session.author || !session.padId) throw new Error('session not ready');
if (!settings.enablePadWideSettings) return;
if (!await padManager.doesPadExist(session.padId)) {
messageLogger.warn(`Ignoring padoptions for missing pad ${session.padId}`);
return;
}
const pad = await padManager.getPad(session.padId, null, session.author);
if (!await isPadCreator(pad, session.author)) {
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
socket.emit('shout', {
type: 'COLLABROOM',
data: {
type: 'shoutMessage',
payload: {
message: {
message: 'Only the pad creator can change pad settings',
sticky: false,
},
timestamp: Date.now(),
},
},
});
return;
}
pad.setPadSettings(message.data.payload.options);
await pad.saveToDatabase();
_getRoomSockets(session.padId).forEach((socket) => {
socket.emit('message', message);
});
};


/**
* Handles a message from a user
Expand Down Expand Up @@ -413,6 +448,11 @@ exports.handleMessage = async (socket:any, message: ClientVarMessage) => {
try {
switch (type) {
case 'suggestUserName': handleSuggestUserName(socket, message as unknown as ClientSuggestUserName); break;
case 'padoptions':
await handlePadOptionsMessage(
socket,
message as unknown as PadOptionsMessage & {data: {payload: PadOptionsMessage}});
break;
default: throw new Error('unknown message type');
}
} catch (err) {
Expand Down Expand Up @@ -883,8 +923,13 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
]);
({colorId: authorColorId, name: authorName} = await authorManager.getAuthor(sessionInfo.author));

const padExisted = await padManager.doesPadExist(sessionInfo.padId);
// load the pad-object from the database
const pad = await padManager.getPad(sessionInfo.padId, null, sessionInfo.author);
if (settings.enablePadWideSettings && !padExisted && message.padSettingsDefaults) {
pad.setPadSettings(message.padSettingsDefaults);
await pad.saveToDatabase();
}

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

// Warning: never ever send sessionInfo.padId to the client. If the client is read only you
// would open a security hole 1 swedish mile wide...
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

:) Great comment

const canEditPadSettings = settings.enablePadWideSettings &&
!sessionInfo.readonly && await isPadCreator(pad, sessionInfo.author);
const clientVars:MapArrayType<any> = {
skinName: settings.skinName,
skinVariants: settings.skinVariants,
Expand All @@ -1033,9 +1080,10 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
maxRevisions: 100,
},
enableDarkMode: settings.enableDarkMode,
enablePadWideSettings: settings.enablePadWideSettings,
automaticReconnectionTimeout: settings.automaticReconnectionTimeout,
initialRevisionList: [],
initialOptions: {},
initialOptions: pad.getPadSettings(),
savedRevisions: pad.getSavedRevisions(),
collab_client_vars: {
initialAttributedText: atext,
Expand All @@ -1060,6 +1108,7 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
numConnectedUsers: roomSockets.length + 1, // +1 for this user (not yet in room)
readOnlyId: sessionInfo.readOnlyPadId,
readonly: sessionInfo.readonly,
canEditPadSettings,
serverTimestamp: Date.now(),
sessionRefreshInterval: settings.cookie.sessionRefreshInterval,
userId: sessionInfo.author,
Expand Down
5 changes: 4 additions & 1 deletion src/node/utils/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export type SettingsType = {
},
updateServer: string,
enableDarkMode: boolean,
enablePadWideSettings: boolean,
skinName: string | null,
skinVariants: string,
ip: string,
Expand Down Expand Up @@ -294,7 +295,7 @@ export type SettingsType = {
lowerCasePadIds: boolean,
randomVersionString: string,
gitVersion: string
getPublicSettings: () => Pick<SettingsType, "title" | "skinVariants"|"randomVersionString"|"skinName"|"toolbar"| "exposeVersion"| "gitVersion">,
getPublicSettings: () => Pick<SettingsType, "title" | "skinVariants"|"randomVersionString"|"skinName"|"toolbar"| "exposeVersion"| "gitVersion" | "enablePadWideSettings">,
}

const settings: SettingsType = {
Expand Down Expand Up @@ -328,6 +329,7 @@ const settings: SettingsType = {
},
updateServer: "https://static.etherpad.org",
enableDarkMode: true,
enablePadWideSettings: false,
/*
* Skin name.
*
Expand Down Expand Up @@ -657,6 +659,7 @@ const settings: SettingsType = {
title: settings.title,
skinName: settings.skinName,
skinVariants: settings.skinVariants,
enablePadWideSettings: settings.enablePadWideSettings,
}
},
gitVersion: getGitCommit(),
Expand Down
13 changes: 7 additions & 6 deletions src/static/js/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ exports.chat = (() => {
let chatMentions = 0;
return {
show() {
if (pad.settings.hideChat) return;
$('#chaticon').removeClass('visible');
$('#chatbox').addClass('visible');
this.scrollDown(true);
Expand All @@ -49,7 +50,7 @@ exports.chat = (() => {
}, 100);
},
// Make chat stick to right hand side of screen
stickToScreen(fromInitialCall) {
stickToScreen(fromInitialCall, persistPreference = true) {
if ($('#options-stickychat').prop('checked')) {
$('#options-stickychat').prop('checked', false);
}
Expand All @@ -65,13 +66,13 @@ exports.chat = (() => {
$('#chatbox').css('display', 'flex');
}, 0);

padcookie.setPref('chatAlwaysVisible', isStuck);
if (persistPreference) padcookie.setPref('chatAlwaysVisible', isStuck);
$('#options-stickychat').prop('checked', isStuck);
},
chatAndUsers(fromInitialCall) {
chatAndUsers(fromInitialCall, persistPreference = true) {
const toEnable = $('#options-chatandusers').is(':checked');
if (toEnable || !userAndChat || fromInitialCall) {
this.stickToScreen(true);
this.stickToScreen(true, persistPreference);
$('#options-stickychat').prop('checked', true);
$('#options-chatandusers').prop('checked', true);
$('#options-stickychat').prop('disabled', true);
Expand All @@ -80,7 +81,7 @@ exports.chat = (() => {
$('#options-stickychat').prop('disabled', false);
userAndChat = false;
}
padcookie.setPref('chatAndUsers', userAndChat);
if (persistPreference) padcookie.setPref('chatAndUsers', userAndChat);
$('#users, .sticky-container')
.toggleClass('chatAndUsers popup-show stickyUsers', userAndChat);
$('#chatbox').toggleClass('chatAndUsersChat', userAndChat);
Expand Down Expand Up @@ -204,7 +205,7 @@ exports.chat = (() => {
count++;
$('#chatcounter').text(count);

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