')
- .text('If the problem persists, please send this error message to your webmaster:'),
- $('
').css('text-align', 'left').css('font-size', '.8em').css('margin-top', '1em')
- .append($('
').addClass('error-msg').text(msg)).append($(' '))
- .append(txt(`at ${url} at line ${linenumber}`)).append($(' '))
- .append(txt(`ErrorId: ${errorId}`)).append($(' '))
- .append(txt(type)).append($(' '))
- .append(txt(`URL: ${window.location.href}`)).append($(' '))
- .append(txt(`UserAgent: ${navigator.userAgent}`)).append($(' ')),
- ];
+ this._seenErrors.add(errorKey);
+ // Hide internal error details from end users unless explicitly in development mode.
+ // Default to hiding details (secure by default) since clientVars.mode may not be
+ // available before the CLIENT_VARS handshake completes.
+ // See https://github.com/ether/etherpad-lite/issues/5765
+ const isProduction = (window as any).clientVars?.mode !== 'development';
+
+ const errorMsg = isProduction
+ ? [
+ $(' ')
+ .append($('').text('Please press and hold Ctrl and press F5 to reload this page')),
+ $('
')
+ .text('If the problem persists, please contact your webmaster.')
+ .append($(' '))
+ .append($('').css('font-size', '.8em').text(`ErrorId: ${errorId}`)),
+ ]
+ : (() => {
+ const txt = document.createTextNode.bind(document);
+ return [
+ $('')
+ .append($('').text('Please press and hold Ctrl and press F5 to reload this page')),
+ $('
')
+ .text('If the problem persists, please send this error message to your webmaster:'),
+ $('
').css('text-align', 'left').css('font-size', '.8em').css('margin-top', '1em')
+ .append($('
').addClass('error-msg').text(msg)).append($(' '))
+ .append(txt(`at ${url} at line ${linenumber}`)).append($(' '))
+ .append(txt(`ErrorId: ${errorId}`)).append($(' '))
+ .append(txt(type)).append($(' '))
+ .append(txt(`URL: ${window.location.href}`)).append($(' '))
+ .append(txt(`UserAgent: ${navigator.userAgent}`)).append($(' ')),
+ ];
+ })();
// @ts-ignore
$.gritter.add({
diff --git a/src/static/js/pluginfw/LinkInstaller.ts b/src/static/js/pluginfw/LinkInstaller.ts
index ddb8835ca..1c56a5c46 100644
--- a/src/static/js/pluginfw/LinkInstaller.ts
+++ b/src/static/js/pluginfw/LinkInstaller.ts
@@ -3,244 +3,243 @@ import path from "path";
import {node_modules, pluginInstallPath} from "./installer";
import {accessSync, constants, rmSync, symlinkSync, unlinkSync} from "node:fs";
import {dependencies, name} from '../../../package.json'
-import {pathToFileURL} from 'node:url';
import settings from '../../../node/utils/Settings';
import {readFileSync} from "fs";
export class LinkInstaller {
- private livePluginManager: PluginManager;
- private loadedPlugins: IPluginInfo[] = [];
- /*
- * A map of dependencies to their dependents
- *
- */
- private readonly dependenciesMap: Map>;
+ private livePluginManager: PluginManager;
+ private loadedPlugins: IPluginInfo[] = [];
+ /*
+ * A map of dependencies to their dependents
+ *
+ */
+ private readonly dependenciesMap: Map>;
- constructor() {
- this.livePluginManager = new PluginManager({
- pluginsPath: pluginInstallPath,
- hostRequire: undefined,
- cwd: path.join(settings.root, 'src')
- });
- this.dependenciesMap = new Map();
+ constructor() {
+ this.livePluginManager = new PluginManager({
+ pluginsPath: pluginInstallPath,
+ hostRequire: undefined,
+ cwd: path.join(settings.root, 'src')
+ });
+ this.dependenciesMap = new Map();
+ }
+
+
+ public async init() {
+ // Insert Etherpad lite dependencies
+ for (let [dependency] of Object.entries(dependencies)) {
+ if (this.dependenciesMap.has(dependency)) {
+ this.dependenciesMap.get(dependency)?.add(name)
+ } else {
+ this.dependenciesMap.set(dependency, new Set([name]))
+ }
}
+ }
+ public async installFromPath(path: string) {
+ const installedPlugin = await this.livePluginManager.installFromPath(path)
+ this.linkDependency(installedPlugin.name)
+ await this.checkLinkedDependencies(installedPlugin)
+ }
- public async init() {
- // Insert Etherpad lite dependencies
- for (let [dependency] of Object.entries(dependencies)) {
- if (this.dependenciesMap.has(dependency)) {
- this.dependenciesMap.get(dependency)?.add(name)
- } else {
- this.dependenciesMap.set(dependency, new Set([name]))
- }
+ public async installFromGitHub(repository: string) {
+ const installedPlugin = await this.livePluginManager.installFromGithub(repository)
+ this.linkDependency(installedPlugin.name)
+ await this.checkLinkedDependencies(installedPlugin)
+ }
+
+ public async installPlugin(pluginName: string, version?: string) {
+ if (version) {
+ const installedPlugin = await this.livePluginManager.install(pluginName, version);
+ this.linkDependency(pluginName)
+ await this.checkLinkedDependencies(installedPlugin)
+ } else {
+ const installedPlugin = await this.livePluginManager.install(pluginName);
+ this.linkDependency(pluginName)
+ await this.checkLinkedDependencies(installedPlugin)
+ }
+ }
+
+ public async listPlugins() {
+ const plugins = this.livePluginManager.list()
+ if (plugins && plugins.length > 0 && this.loadedPlugins.length == 0) {
+ this.loadedPlugins = plugins
+ // Check already installed plugins
+ for (let plugin of plugins) {
+ await this.checkLinkedDependencies(plugin)
+ }
+ }
+ return plugins
+ }
+
+ public async uninstallPlugin(pluginName: string) {
+ const installedPlugin = this.livePluginManager.getInfo(pluginName)
+ if (installedPlugin) {
+ console.debug(`Uninstalling plugin ${pluginName}`)
+ await this.removeSymlink(installedPlugin)
+ await this.livePluginManager.uninstall(pluginName)
+ await this.removeSubDependencies(installedPlugin)
+ }
+ }
+
+ private async removeSubDependencies(plugin: IPluginInfo) {
+ const pluginDependencies = Object.keys(plugin.dependencies)
+ console.debug("Removing sub dependencies",pluginDependencies)
+ for (let dependency of pluginDependencies) {
+ await this.removeSubDependency(plugin.name, dependency)
+ }
+ }
+
+ private async removeSubDependency(_name: string, dependency:string) {
+ if (this.dependenciesMap.has(dependency)) {
+ console.debug(`Dependency ${dependency} is still being used by other plugins`)
+ return
+ }
+ // Read sub dependencies
+ try {
+ const json:IPluginInfo = JSON.parse(
+ readFileSync(path.join(pluginInstallPath, dependency, 'package.json'), 'utf-8'));
+ if(json.dependencies){
+ for (let [subDependency] of Object.entries(json.dependencies)) {
+ await this.removeSubDependency(dependency, subDependency)
}
- }
+ }
+ } catch (e){}
+ this.uninstallDependency(dependency)
+ }
- public async installFromPath(path: string) {
- const installedPlugin = await this.livePluginManager.installFromPath(path)
- this.linkDependency(installedPlugin.name)
- await this.checkLinkedDependencies(installedPlugin)
+ private uninstallDependency(dependency: string) {
+ try {
+ console.debug(`Uninstalling dependency ${dependency}`)
+ // Check if the dependency is already installed
+ accessSync(path.join(pluginInstallPath, dependency), constants.F_OK)
+ rmSync(path.join(pluginInstallPath, dependency), {
+ force: true,
+ recursive: true
+ })
+ } catch (err) {
+ // Symlink does not exist
+ // So nothing to do
}
+ }
- public async installFromGitHub(repository: string) {
- const installedPlugin = await this.livePluginManager.installFromGithub(repository)
- this.linkDependency(installedPlugin.name)
- await this.checkLinkedDependencies(installedPlugin)
+ private async removeSymlink(plugin: IPluginInfo) {
+ try {
+ accessSync(path.join(node_modules, plugin.name), constants.F_OK)
+ await this.unlinkSubDependencies(plugin)
+ // Remove the plugin itself
+ this.unlinkDependency(plugin.name)
+ } catch (err) {
+ console.error(`Symlink for ${plugin.name} does not exist`)
+ // Symlink does not exist
+ // So nothing to do
}
+ }
- public async installPlugin(pluginName: string, version?: string) {
- if (version) {
- const installedPlugin = await this.livePluginManager.install(pluginName, version);
- this.linkDependency(pluginName)
- await this.checkLinkedDependencies(installedPlugin)
- } else {
- const installedPlugin = await this.livePluginManager.install(pluginName);
- this.linkDependency(pluginName)
- await this.checkLinkedDependencies(installedPlugin)
+ private async unlinkSubDependencies(plugin: IPluginInfo) {
+ const pluginDependencies = Object.keys(plugin.dependencies)
+ for (let dependency of pluginDependencies) {
+ this.dependenciesMap.get(dependency)?.delete(plugin.name)
+ await this.unlinkSubDependency(plugin.name, dependency)
+ }
+ }
+
+ private async unlinkSubDependency(plugin: string, dependency: string) {
+ if (this.dependenciesMap.has(dependency)) {
+ this.dependenciesMap.get(dependency)?.delete(plugin)
+ if (this.dependenciesMap.get(dependency)!.size > 0) {
+ // We have other dependants so do not uninstall
+ return
+ }
+ }
+ this.unlinkDependency(dependency)
+ // Read sub dependencies
+ try {
+ const json:IPluginInfo = JSON.parse(
+ readFileSync(path.join(pluginInstallPath, dependency, 'package.json'), 'utf-8'));
+ if(json.dependencies){
+ for (let [subDependency] of Object.entries(json.dependencies)) {
+ await this.unlinkSubDependency(dependency, subDependency)
}
- }
+ }
+ } catch (e){}
- public async listPlugins() {
- const plugins = this.livePluginManager.list()
- if (plugins && plugins.length > 0 && this.loadedPlugins.length == 0) {
- this.loadedPlugins = plugins
- // Check already installed plugins
- for (let plugin of plugins) {
- await this.checkLinkedDependencies(plugin)
- }
- }
- return plugins
- }
+ console.debug("Unlinking sub dependency",dependency)
+ this.dependenciesMap.delete(dependency)
+ }
- public async uninstallPlugin(pluginName: string) {
- const installedPlugin = this.livePluginManager.getInfo(pluginName)
- if (installedPlugin) {
- console.debug(`Uninstalling plugin ${pluginName}`)
- await this.removeSymlink(installedPlugin)
- await this.livePluginManager.uninstall(pluginName)
- await this.removeSubDependencies(installedPlugin)
- }
- }
- private async removeSubDependencies(plugin: IPluginInfo) {
- const pluginDependencies = Object.keys(plugin.dependencies)
- console.debug("Removing sub dependencies",pluginDependencies)
- for (let dependency of pluginDependencies) {
- await this.removeSubDependency(plugin.name, dependency)
- }
+ private async addSubDependencies(plugin: IPluginInfo) {
+ const pluginDependencies = Object.keys(plugin.dependencies)
+ for (let dependency of pluginDependencies) {
+ await this.addSubDependency(plugin.name, dependency)
}
+ }
- private async removeSubDependency(_name: string, dependency:string) {
- if (this.dependenciesMap.has(dependency)) {
- console.debug(`Dependency ${dependency} is still being used by other plugins`)
- return
- }
+ private async addSubDependency(plugin: string, dependency: string) {
+ if (this.dependenciesMap.has(dependency)) {
+ // We already added the sub dependency
+ this.dependenciesMap.get(dependency)?.add(plugin)
+ } else {
+
+ try {
+ this.linkDependency(dependency)
// Read sub dependencies
- try {
- const json:IPluginInfo = JSON.parse(
- readFileSync(pathToFileURL(path.join(pluginInstallPath, dependency, 'package.json'))) as unknown as string);
- if(json.dependencies){
- for (let [subDependency] of Object.entries(json.dependencies)) {
- await this.removeSubDependency(dependency, subDependency)
- }
- }
- } catch (e){}
- this.uninstallDependency(dependency)
- }
-
- private uninstallDependency(dependency: string) {
- try {
- console.debug(`Uninstalling dependency ${dependency}`)
- // Check if the dependency is already installed
- accessSync(path.join(pluginInstallPath, dependency), constants.F_OK)
- rmSync(path.join(pluginInstallPath, dependency), {
- force: true,
- recursive: true
- })
- } catch (err) {
- // Symlink does not exist
- // So nothing to do
+ const json:IPluginInfo = JSON.parse(
+ readFileSync(path.join(pluginInstallPath, dependency, 'package.json'), 'utf-8'));
+ if(json.dependencies){
+ Object.keys(json.dependencies).forEach((subDependency: string) => {
+ this.addSubDependency(dependency, subDependency)
+ })
}
+ this.dependenciesMap.set(dependency, new Set([plugin]))
+ } catch (err) {
+ console.error(`Error reading package.json ${err} for ${path.join(pluginInstallPath, dependency, 'package.json')}`)
+ }
}
+ }
- private async removeSymlink(plugin: IPluginInfo) {
- try {
- accessSync(path.join(node_modules, plugin.name), constants.F_OK)
- await this.unlinkSubDependencies(plugin)
- // Remove the plugin itself
- this.unlinkDependency(plugin.name)
- } catch (err) {
- console.error(`Symlink for ${plugin.name} does not exist`)
- // Symlink does not exist
- // So nothing to do
- }
- }
-
- private async unlinkSubDependencies(plugin: IPluginInfo) {
- const pluginDependencies = Object.keys(plugin.dependencies)
- for (let dependency of pluginDependencies) {
- this.dependenciesMap.get(dependency)?.delete(plugin.name)
- await this.unlinkSubDependency(plugin.name, dependency)
- }
- }
-
- private async unlinkSubDependency(plugin: string, dependency: string) {
- if (this.dependenciesMap.has(dependency)) {
- this.dependenciesMap.get(dependency)?.delete(plugin)
- if (this.dependenciesMap.get(dependency)!.size > 0) {
- // We have other dependants so do not uninstall
- return
- }
- }
- this.unlinkDependency(dependency)
- // Read sub dependencies
- try {
- const json:IPluginInfo = JSON.parse(
- readFileSync(pathToFileURL(path.join(pluginInstallPath, dependency, 'package.json'))) as unknown as string);
- if(json.dependencies){
- for (let [subDependency] of Object.entries(json.dependencies)) {
- await this.unlinkSubDependency(dependency, subDependency)
- }
- }
- } catch (e){}
-
- console.debug("Unlinking sub dependency",dependency)
- this.dependenciesMap.delete(dependency)
- }
-
-
- private async addSubDependencies(plugin: IPluginInfo) {
- const pluginDependencies = Object.keys(plugin.dependencies)
- for (let dependency of pluginDependencies) {
- await this.addSubDependency(plugin.name, dependency)
- }
- }
-
- private async addSubDependency(plugin: string, dependency: string) {
- if (this.dependenciesMap.has(dependency)) {
- // We already added the sub dependency
- this.dependenciesMap.get(dependency)?.add(plugin)
+ private linkDependency(dependency: string) {
+ try {
+ // Check if the dependency is already installed
+ accessSync(path.join(node_modules, dependency), constants.F_OK)
+ } catch (err) {
+ try {
+ if(dependency.startsWith("@")){
+ const newDependency = dependency.split("@")[0]
+ symlinkSync(path.join(pluginInstallPath, dependency), path.join(node_modules, newDependency), 'dir')
} else {
-
- try {
- this.linkDependency(dependency)
- // Read sub dependencies
- const json:IPluginInfo = JSON.parse(
- readFileSync(pathToFileURL(path.join(pluginInstallPath, dependency, 'package.json'))) as unknown as string);
- if(json.dependencies){
- Object.keys(json.dependencies).forEach((subDependency: string) => {
- this.addSubDependency(dependency, subDependency)
- })
- }
- } catch (err) {
- console.error(`Error reading package.json ${err} for ${pathToFileURL(path.join(pluginInstallPath, dependency, 'package.json')).toString()}`)
- }
- this.dependenciesMap.set(dependency, new Set([plugin]))
+ symlinkSync(path.join(pluginInstallPath, dependency), path.join(node_modules, dependency), 'dir')
}
+ } catch (e) {
+ // Nothing to do. We're all set
+ }
}
+ }
- private linkDependency(dependency: string) {
- try {
- // Check if the dependency is already installed
- accessSync(path.join(node_modules, dependency), constants.F_OK)
- } catch (err) {
- try {
- if(dependency.startsWith("@")){
- const newDependency = dependency.split("@")[0]
- symlinkSync(path.join(pluginInstallPath, dependency), path.join(node_modules, newDependency), 'dir')
- } else {
- symlinkSync(path.join(pluginInstallPath, dependency), path.join(node_modules, dependency), 'dir')
- }
- } catch (e) {
- // Nothing to do. We're all set
- }
- }
- }
-
- private unlinkDependency(dependency: string) {
- try {
- // Check if the dependency is already installed
- accessSync(path.join(node_modules, dependency), constants.F_OK)
- unlinkSync(path.join(node_modules, dependency))
- } catch (err) {
- // Symlink does not exist
- // So nothing to do
- }
+ private unlinkDependency(dependency: string) {
+ try {
+ // Check if the dependency is already installed
+ accessSync(path.join(node_modules, dependency), constants.F_OK)
+ unlinkSync(path.join(node_modules, dependency))
+ } catch (err) {
+ // Symlink does not exist
+ // So nothing to do
}
+ }
- private async checkLinkedDependencies(plugin: IPluginInfo) {
- // Check if the plugin really exists at source
- try {
- accessSync(path.join(pluginInstallPath, plugin.name), constants.F_OK)
- // Skip if the plugin is already linked
- } catch (err) {
- // The plugin is not installed
- console.debug(`Plugin ${plugin.name} is not installed`)
- }
- await this.addSubDependencies(plugin)
- this.dependenciesMap.set(plugin.name, new Set())
+ private async checkLinkedDependencies(plugin: IPluginInfo) {
+ // Check if the plugin really exists at source
+ try {
+ accessSync(path.join(pluginInstallPath, plugin.name), constants.F_OK)
+ // Skip if the plugin is already linked
+ } catch (err) {
+ // The plugin is not installed
+ console.debug(`Plugin ${plugin.name} is not installed`)
}
+ await this.addSubDependencies(plugin)
+ this.dependenciesMap.set(plugin.name, new Set())
+ }
}
diff --git a/src/static/js/pluginfw/installer.ts b/src/static/js/pluginfw/installer.ts
index a85bc77f1..73f4195d5 100644
--- a/src/static/js/pluginfw/installer.ts
+++ b/src/static/js/pluginfw/installer.ts
@@ -124,7 +124,11 @@ export const checkForMigration = async () => {
for (const plugin of installedPlugins.plugins) {
if (plugin.name.startsWith(plugins.prefix) && plugin.name !== 'ep_etherpad-lite') {
- await linkInstaller.installPlugin(plugin.name, plugin.version);
+ try {
+ await linkInstaller.installPlugin(plugin.name, plugin.version);
+ } catch (e) {
+ logger.error(`Error installing plugin ${plugin.name} with version ${plugin.version}: ${e}`);
+ }
}
}
};
@@ -174,7 +178,18 @@ export const getAvailablePlugins = async (maxCacheAge: number | false) => {
}
const pluginsLoaded: AxiosResponse> = await axios.get(`${settings.updateServer}/plugins.json`, {headers})
- availablePlugins = pluginsLoaded.data;
+ const data = pluginsLoaded.data;
+ // Normalize: the registry may use numeric keys instead of plugin names
+ const normalized: MapArrayType = {};
+ for (const key in data) {
+ const entry = data[key];
+ if (entry && entry.name) {
+ normalized[entry.name] = entry;
+ } else {
+ normalized[key] = entry;
+ }
+ }
+ availablePlugins = normalized;
cacheTimestamp = nowTimestamp;
return availablePlugins;
};
diff --git a/src/static/js/timeslider.ts b/src/static/js/timeslider.ts
index befc4f8b7..d0e45973f 100644
--- a/src/static/js/timeslider.ts
+++ b/src/static/js/timeslider.ts
@@ -33,6 +33,39 @@ import padutils from './pad_utils'
const socketio = require('./socketio');
import html10n from '../js/vendors/html10n'
let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider;
+let cp = '';
+const playbackSpeedCookie = 'timesliderPlaybackSpeed';
+
+const getPrefsCookieName = () => `${cp}${window.location.protocol === 'https:' ? 'prefs' : 'prefsHttp'}`;
+
+const readPadPrefs = () => {
+ try {
+ let json = Cookies.get(getPrefsCookieName());
+ if (json == null) {
+ const unprefixed = window.location.protocol === 'https:' ? 'prefs' : 'prefsHttp';
+ if (unprefixed !== getPrefsCookieName()) json = Cookies.get(unprefixed);
+ }
+ return json == null ? {} : JSON.parse(json);
+ } catch (err) {
+ return {};
+ }
+};
+
+const writePadPrefs = (prefs) => {
+ Cookies.set(getPrefsCookieName(), JSON.stringify(prefs), {expires: 365 * 100});
+};
+
+const setPadPref = (prefName, value) => {
+ const prefs = readPadPrefs();
+ prefs[prefName] = value;
+ writePadPrefs(prefs);
+};
+
+const applyShowLineNumbers = (showLineNumbers) => {
+ padutils.setCheckbox($('#options-linenoscheck'), showLineNumbers);
+ $('body').toggleClass('line-numbers-hidden', !showLineNumbers);
+ window.requestAnimationFrame(() => $(window).trigger('resize'));
+};
const init = () => {
padutils.setupGlobalExceptionHandler();
@@ -48,10 +81,11 @@ const init = () => {
document.title = `${padId.replace(/_+/g, ' ')} | ${document.title}`;
// ensure we have a token
- token = Cookies.get('token');
+ cp = (window as any).clientVars?.cookiePrefix || '';
+ token = Cookies.get(`${cp}token`) || Cookies.get('token');
if (token == null) {
token = `t.${randomString()}`;
- Cookies.set('token', token, {expires: 60});
+ Cookies.set(`${cp}token`, token, {expires: 60});
}
socket = socketio.connect(exports.baseURL, '/', {query: {padId}});
@@ -101,7 +135,7 @@ const sendSocketMsg = (type, data) => {
data,
padId,
token,
- sessionID: Cookies.get('sessionID'),
+ sessionID: Cookies.get(`${cp}sessionID`) || Cookies.get('sessionID'),
});
};
@@ -110,6 +144,7 @@ const fireWhenAllScriptsAreLoaded = [];
const handleClientVars = (message) => {
// save the client Vars
window.clientVars = message.data;
+ cp = (window as any).clientVars?.cookiePrefix || '';
if (window.clientVars.sessionRefreshInterval) {
const ping =
@@ -165,11 +200,26 @@ const handleClientVars = (message) => {
$('#playpause_button_icon').attr('title', html10n.get('timeslider.playPause'));
$('#leftstep').attr('title', html10n.get('timeslider.backRevision'));
$('#rightstep').attr('title', html10n.get('timeslider.forwardRevision'));
+ padutils.bindCheckboxChange($('#options-linenoscheck'), () => {
+ const showLineNumbers = padutils.getCheckbox('#options-linenoscheck');
+ setPadPref('showLineNumbers', showLineNumbers);
+ applyShowLineNumbers(showLineNumbers);
+ });
+ applyShowLineNumbers(readPadPrefs().showLineNumbers !== false);
// font family change
$('#viewfontmenu').on('change', function () {
$('#innerdocbody').css('font-family', $(this).val() || '');
});
+
+ const savedPlaybackSpeed = Cookies.get(`${cp}${playbackSpeedCookie}`) || '100';
+ $('#playbackspeed').val(savedPlaybackSpeed);
+ BroadcastSlider.setPlaybackSpeed(savedPlaybackSpeed);
+ $('#playbackspeed').on('change', function () {
+ const speed = String($(this).val() || '100');
+ Cookies.set(`${cp}${playbackSpeedCookie}`, speed);
+ BroadcastSlider.setPlaybackSpeed(speed);
+ });
};
exports.baseURL = '';
diff --git a/src/static/js/types/SocketIOMessage.ts b/src/static/js/types/SocketIOMessage.ts
index f2b8cfc14..08be6a03e 100644
--- a/src/static/js/types/SocketIOMessage.ts
+++ b/src/static/js/types/SocketIOMessage.ts
@@ -73,20 +73,22 @@ export type ClientVarPayload = {
chatHead: number,
readonly: boolean,
serverTimestamp: number,
- initialOptions: MapArrayType,
+ initialOptions: PadOption,
userId: string,
+ canEditPadSettings?: boolean,
+ enablePadWideSettings?: boolean,
mode: string,
randomVersionString: string,
skinName: string
skinVariants: string,
exportAvailable: string
+ docxExport: boolean
savedRevisions: PadRevision[],
initialRevisionList: number[],
padShortcutEnabled: MapArrayType,
initialTitle: string,
opts: {}
numConnectedUsers: number
- abiwordAvailable: string
sofficeAvailable: string
plugins: {
plugins: MapArrayType
@@ -184,6 +186,7 @@ export type ClientReadyMessage = {
sessionID: string,
token: string,
userInfo: UserInfo,
+ padSettingsDefaults?: PadOption,
reconnect?: boolean
client_rev?: number
}
@@ -249,7 +252,7 @@ export type PadOption = {
"alwaysShowChat"?: boolean,
"chatAndUsers"?: boolean,
"lang"?: null|string,
- view? : MapArrayType
+ view? : MapArrayType
}
@@ -322,4 +325,3 @@ export type SocketClientReadyMessage = {
reconnect?: boolean
client_rev?: number
}
-
diff --git a/src/static/js/undomodule.ts b/src/static/js/undomodule.ts
index c3bdd2fab..542fb7157 100644
--- a/src/static/js/undomodule.ts
+++ b/src/static/js/undomodule.ts
@@ -212,13 +212,7 @@ const undoModule = (() => {
}
}
if (!merged) {
- /*
- * Push the event on the undo stack only if it exists, and if it's
- * not a "clearauthorship". This disallows undoing the removal of the
- * authorship colors, but is a necessary stopgap measure against
- * https://github.com/ether/etherpad-lite/issues/2802
- */
- if (event && (event.eventType !== 'clearauthorship')) {
+ if (event) {
stack.pushEvent(event);
}
}
diff --git a/src/static/js/vendors/html10n.ts b/src/static/js/vendors/html10n.ts
index 156ab75f6..3f72de4c2 100644
--- a/src/static/js/vendors/html10n.ts
+++ b/src/static/js/vendors/html10n.ts
@@ -995,8 +995,7 @@ export default html10n
// @ts-ignore
window.html10n = html10n
-// gettext-like shortcut
-if (window._ === undefined){
- // @ts-ignore
- window._ = html10n.get;
-}
+// gettext-like shortcut — always set this so plugins can use window._() for localization.
+// Internal code uses underscore via require(), not window._, so this is safe.
+// @ts-ignore
+window._ = html10n.get;
diff --git a/src/static/js/welcome.ts b/src/static/js/welcome.ts
index dacaed7bf..f4e87427d 100644
--- a/src/static/js/welcome.ts
+++ b/src/static/js/welcome.ts
@@ -9,6 +9,8 @@ function getCookie(name: string) {
}
+const cp = (window as any).clientVars?.cookiePrefix || '';
+
function handleTransferOfSession() {
const transferNowButton = document.querySelector('[data-l10n-id="index.transferSessionNow"]')! as HTMLButtonElement;
@@ -25,8 +27,8 @@ function handleTransferOfSession() {
"Content-Type": "application/json"
},
body: JSON.stringify({
- prefsHttp: getCookie('prefsHttp'),
- token: getCookie('token'),
+ prefsHttp: getCookie(`${cp}prefsHttp`) || getCookie('prefsHttp'),
+ token: getCookie(`${cp}token`) || getCookie('token'),
})
})
diff --git a/src/static/skins/colibris/src/components/buttons.css b/src/static/skins/colibris/src/components/buttons.css
index c9c3e0c2a..aea8f1e56 100644
--- a/src/static/skins/colibris/src/components/buttons.css
+++ b/src/static/skins/colibris/src/components/buttons.css
@@ -55,3 +55,20 @@ button, .btn
box-shadow: none;
cursor: not-allowed;
}
+
+.btn-danger {
+ background: #d1242f;
+ color: #fff;
+}
+
+.btn-danger:hover {
+ background: #b71c26;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.08), 0 1px 2px rgba(0,0,0,0.12);
+ transform: translateY(-1px);
+}
+
+.btn-danger:disabled {
+ background: #aaa;
+ color: #fff;
+ cursor: not-allowed;
+}
diff --git a/src/static/skins/colibris/src/components/import-export.css b/src/static/skins/colibris/src/components/import-export.css
index d8425d895..0465e20b5 100644
--- a/src/static/skins/colibris/src/components/import-export.css
+++ b/src/static/skins/colibris/src/components/import-export.css
@@ -1,15 +1,3 @@
-#importmessageabiword {
- font-style: italic;
- color: #64d29b;
- color: var(--primary-color);
-}
-#importmessageabiword > a {
- font-weight: bold;
- text-decoration: underline;
- color: #64d29b;
- color: var(--primary-color);
-}
-
#importmessagefail {
margin-top: 10px;
}
diff --git a/src/static/skins/colibris/src/components/popup.css b/src/static/skins/colibris/src/components/popup.css
index db6647431..381c10d87 100644
--- a/src/static/skins/colibris/src/components/popup.css
+++ b/src/static/skins/colibris/src/components/popup.css
@@ -26,6 +26,20 @@
color: var(--text-color);
}
+.settings-sections {
+ display: grid;
+ gap: 24px;
+}
+
+.settings-section {
+ margin-bottom: 20px;
+ min-width: 0;
+}
+
+#settings .settings-section > h2 {
+ margin-top: 0;
+}
+
.popup:not(.comment-modal) p {
margin: 10px 0;
}
@@ -41,6 +55,21 @@
min-width: 180px;
}
+.settings-notice {
+ color: var(--text-color);
+ font-size: 0.95rem;
+ line-height: 1.4;
+ border-left: 3px solid var(--primary-color);
+ padding-left: 10px;
+}
+
+@media (min-width: 1100px) {
+ .settings-sections.has-pad-settings {
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ align-items: start;
+ }
+}
+
@media (prefers-reduced-motion) {
.popup>.popup-content {
transform: scale(1);
@@ -83,45 +112,5 @@
}
#delete-pad {
- background-color: #ff7b72;
-}
-
-#theme-switcher div {
- position: relative;
- width: 30px;
- background-color: white;
- height: 10px;
- border-radius: 5px;
- align-self: center;
-}
-
-#theme-switcher {
- display: flex;
margin-top: 20px;
- flex-direction: row;
-}
-
-#theme-switcher div span {
- width: 15px;
- display: block;
- height: 15px;
- border-radius: 20px;
- position: absolute;
- top: -2px;
- background-color: white;
- transition: background-color 0.25s;
-}
-
-html.super-light-editor #theme-switcher div {
- background-color: #ccc;
-}
-
-html.super-light-editor #theme-switcher div span {
- left: 0;
- background-color: var(--primary-color);;
-}
-
-html.super-dark-editor #theme-switcher div span {
- right: 0;
- background-color: var(--primary-color);;
}
diff --git a/src/static/skins/colibris/src/plugins/brightcolorpicker.css b/src/static/skins/colibris/src/plugins/brightcolorpicker.css
index 8db2880c1..f267888f7 100644
--- a/src/static/skins/colibris/src/plugins/brightcolorpicker.css
+++ b/src/static/skins/colibris/src/plugins/brightcolorpicker.css
@@ -1,14 +1,14 @@
#colorpicker a.brightColorPicker-cancelButton {
- background: none;
- padding: 0;
- padding-top: 10px;
- font-weight: bold;
- border: none;
+ background: none;
+ padding: 0;
+ padding-top: 10px;
+ font-weight: bold;
+ border: none;
}
.brightColorPicker-colorPanel {
- background-color: white !important;
- box-shadow: 0 0 0 1px rgba(99, 114, 130, 0.16), 0 8px 16px rgba(27, 39, 51, 0.08) !important;
- border-radius: 3px !important;
- padding: 15px !important;
+ background-color: white !important;
+ box-shadow: 0 0 0 1px rgba(99, 114, 130, 0.16), 0 8px 16px rgba(27, 39, 51, 0.08) !important;
+ border-radius: 3px !important;
+ padding: 15px !important;
}
\ No newline at end of file
diff --git a/src/static/skins/colibris/timeslider.css b/src/static/skins/colibris/timeslider.css
index 263e0e592..dc52de73c 100644
--- a/src/static/skins/colibris/timeslider.css
+++ b/src/static/skins/colibris/timeslider.css
@@ -82,6 +82,16 @@
font-size: .9em;
}
+.timeslider #outerdocbody > #sidediv {
+ padding-top: 30px;
+ padding-bottom: 30px;
+}
+
+.timeslider #outerdocbody > #innerdocbody {
+ padding-top: 30px;
+ padding-bottom: 30px;
+}
+
@media (max-width: 800px) {
#slider-btn-container {
@@ -95,4 +105,4 @@
#slider-btn-container #playpause_button_icon:before {
font-size: 18px;
}
-}
\ No newline at end of file
+}
diff --git a/src/templates/indexBootstrap.js b/src/templates/indexBootstrap.js
index 6836a460a..d8c2f8b98 100644
--- a/src/templates/indexBootstrap.js
+++ b/src/templates/indexBootstrap.js
@@ -1,5 +1,6 @@
(async () => {
+ window.clientVars = { cookiePrefix: <%-JSON.stringify(settings.cookie.prefix)%> };
window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery;
require('ep_etherpad-lite/static/js/l10n')
require('ep_etherpad-lite/static/js/index')
diff --git a/src/templates/javascript.html b/src/templates/javascript.html
index d93692b09..f3b5e038b 100644
--- a/src/templates/javascript.html
+++ b/src/templates/javascript.html
@@ -1,51 +1,51 @@
-
- JavaScript license information
-
-
-
-
-
-
-
-
+
+ JavaScript license information
+
+
+
+
+
+
+
+
diff --git a/src/templates/pad.html b/src/templates/pad.html
index eb93196e2..5e593f6d7 100644
--- a/src/templates/pad.html
+++ b/src/templates/pad.html
@@ -84,7 +84,7 @@
-
+
<% e.begin_block("permissionDenied"); %>
@@ -115,68 +115,133 @@