Rewrote server in typescript.

feature/typescript
SamTV12345 2023-06-23 18:57:36 +02:00
parent 331cf3d79f
commit 3c2129b1cc
No known key found for this signature in database
GPG Key ID: E63EEC7466038043
57 changed files with 668 additions and 490 deletions

3
src/.gitignore vendored
View File

@ -1 +1,2 @@
dist
dist/
node_modules

View File

@ -21,7 +21,7 @@
import Changeset from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import CustomError from '../utils/customError';
import {CustomError} from '../utils/customError';
import {doesPadExist, getPad, isValidPadId, listAllPads} from './PadManager';
import {
handleCustomMessage,
@ -40,11 +40,11 @@ import {
} from './GroupManager';
import {createAuthor, createAuthorIfNotExistsFor, getAuthorName, listPadsOfAuthor} from './AuthorManager';
import {} from './SessionManager';
import exportHtml from '../utils/ExportHtml';
import exportTxt from '../utils/ExportTxt';
import importHtml from '../utils/ImportHtml';
import {getTXTFromAtext} from '../utils/ExportTxt';
import {setPadHTML} from '../utils/ImportHtml';
const cleanText = require('./Pad').cleanText;
import PadDiff from '../utils/padDiff';
import {PadDiff} from '../utils/padDiff';
import {getPadHTMLDocument} from "../utils/ExportHtml";
/* ********************
* GROUP FUNCTIONS ****
@ -192,7 +192,7 @@ export const getText = async (padID, rev) => {
}
// the client wants the latest text, lets return it to him
const text = exportTxt.getTXTFromAtext(pad, pad.atext);
const text = getTXTFromAtext(pad, pad.atext);
return {text};
};
@ -263,7 +263,7 @@ export const getHTML = async (padID, rev) => {
}
// get the html of this revision
let html = await exportHtml.getPadHTML(pad, rev);
let html = await getPadHTMLDocument(pad, rev);
// wrap the HTML
html = `<!DOCTYPE HTML><html><body>${html}</body></html>`;
@ -289,7 +289,7 @@ export const setHTML = async (padID, html, authorId = '') => {
// add a new changeset with the new html to the pad
try {
await importHtml.setPadHTML(pad, cleanText(html), authorId);
await setPadHTML(pad, cleanText(html), authorId);
} catch (e) {
throw new CustomError('HTML is malformed', 'apierror');
}

View File

@ -20,7 +20,7 @@
*/
import {db} from './DB';
import CustomError from '../utils/customError';
import {CustomError} from '../utils/customError';
import hooks from '../../static/js/pluginfw/hooks.js';
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
@ -283,7 +283,7 @@ export const addPad = async (authorID: string, padID: string) => {
* @param {String} authorID The id of the author
* @param {String} padID The id of the pad the author contributes to
*/
export const removePad = async (authorID: string, padID: string) => {
export const removePad = async (authorID: string, padID?: string) => {
const author = await db.get(`globalAuthor:${authorID}`);
if (author == null) return;

View File

@ -19,7 +19,7 @@
* limitations under the License.
*/
import CustomError from '../utils/customError';
import {CustomError} from '../utils/customError';
const randomString = require('../../static/js/pad_utils').randomString;
import {db} from './DB';
import {doesPadExist, getPad} from './PadManager';

View File

@ -7,22 +7,21 @@ import AttributeMap from '../../static/js/AttributeMap';
import Changeset from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import {AttributePool} from '../../static/js/AttributePool';
import Stream from '../utils/Stream';
import {Stream} from '../utils/Stream';
import assert, {strict} from 'assert'
import {db} from './DB';
import {defaultPadText} from '../utils/Settings';
import {addPad, getAuthorColorId, getAuthorName, getColorPalette, removePad} from './AuthorManager';
import {Revision} from "../models/Revision";
const padManager = require('./PadManager');
const padMessageHandler = require('../handler/PadMessageHandler');
const groupManager = require('./GroupManager');
const CustomError = require('../utils/customError');
const readOnlyManager = require('./ReadOnlyManager');
const randomString = require('../utils/randomstring');
const hooks = require('../../static/js/pluginfw/hooks');
const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
const promises = require('../utils/promises');
import {doesPadExist, getPad} from './PadManager';
import {kickSessionsFromPad} from '../handler/PadMessageHandler';
import {doesGroupExist} from './GroupManager';
import {CustomError} from '../utils/customError';
import {getReadOnlyId} from './ReadOnlyManager';
import {randomString} from '../utils/randomstring';
import hooks from '../../static/js/pluginfw/hooks';
import {timesLimit} from '../utils/promises';
import {padutils} from '../../static/js/pad_utils';
/**
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
* line breaks and convert Tabs to spaces
@ -114,11 +113,11 @@ export class Pad {
pad: this,
authorId,
get author() {
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
padutils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
return this.authorId;
},
set author(authorId) {
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
padutils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
this.authorId = authorId;
},
...this.head === 0 ? {} : {
@ -403,16 +402,16 @@ export class Pad {
for (const p of new Stream(promises).batch(100).buffer(99)) await p;
// Initialize the new pad (will update the listAllPads cache)
const dstPad = await padManager.getPad(destinationID, null);
const dstPad = await getPad(destinationID, null);
// let the plugins know the pad was copied
await hooks.aCallAll('padCopy', {
get originalPad() {
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
padutils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
return this.srcPad;
},
get destinationID() {
warnDeprecated(
padutils.warnDeprecated(
'padCopy destinationID context property is deprecated; use dstPad.id instead');
return this.dstPad.id;
},
@ -428,7 +427,7 @@ export class Pad {
if (destinationID.indexOf('$') >= 0) {
destGroupID = destinationID.split('$')[0];
const groupExists = await groupManager.doesGroupExist(destGroupID);
const groupExists = await doesGroupExist(destGroupID);
// group does not exist
if (!groupExists) {
@ -440,7 +439,7 @@ export class Pad {
async removePadIfForceIsTrueAndAlreadyExist(destinationID, force) {
// if the pad exists, we should abort, unless forced.
const exists = await padManager.doesPadExist(destinationID);
const exists = await doesPadExist(destinationID);
// allow force to be a string
if (typeof force === 'string') {
@ -456,7 +455,7 @@ export class Pad {
}
// exists and forcing
const pad = await padManager.getPad(destinationID);
const pad = await getPad(destinationID);
await pad.remove();
}
}
@ -485,7 +484,7 @@ export class Pad {
}
// initialize the pad with a new line to avoid getting the defaultText
const dstPad = await padManager.getPad(destinationID, '\n', authorId);
const dstPad = await getPad(destinationID, '\n', authorId);
dstPad.pool = this.pool.clone();
const oldAText = this.atext;
@ -509,11 +508,11 @@ export class Pad {
await hooks.aCallAll('padCopy', {
get originalPad() {
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
padutils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
return this.srcPad;
},
get destinationID() {
warnDeprecated(
padutils.warnDeprecated(
'padCopy destinationID context property is deprecated; use dstPad.id instead');
return this.dstPad.id;
},
@ -529,7 +528,7 @@ export class Pad {
const p = [];
// kick everyone from this pad
padMessageHandler.kickSessionsFromPad(padID);
kickSessionsFromPad(padID);
// delete all relations - the original code used async.parallel but
// none of the operations except getting the group depended on callbacks
@ -550,18 +549,18 @@ export class Pad {
}
// remove the readonly entries
p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID) => {
p.push(getReadOnlyId(padID).then(async (readonlyID) => {
await db.remove(`readonly2pad:${readonlyID}`);
}));
p.push(db.remove(`pad2readonly:${padID}`));
// delete all chat messages
p.push(promises.timesLimit(this.chatHead + 1, 500, async (i) => {
p.push(timesLimit(this.chatHead + 1, 500, async (i) => {
await this.db.remove(`pad:${this.id}:chat:${i}`, null);
}));
// delete all revisions
p.push(promises.timesLimit(this.head + 1, 500, async (i) => {
p.push(timesLimit(this.head + 1, 500, async (i) => {
await this.db.remove(`pad:${this.id}:revs:${i}`, null);
}));
@ -571,10 +570,10 @@ export class Pad {
});
// delete the pad entry and delete pad from padManager
p.push(padManager.removePad(padID));
p.push(removePad(padID));
p.push(hooks.aCallAll('padRemove', {
get padID() {
warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
padutils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
return this.pad.id;
},
pad: this,

View File

@ -19,7 +19,7 @@
* limitations under the License.
*/
import CustomError from '../utils/customError';
import {CustomError} from '../utils/customError';
import {Pad} from './Pad';
import {db} from './DB';

View File

@ -21,7 +21,7 @@
import {db} from './DB';
import randomString from '../utils/randomstring';
import {randomString} from '../utils/randomstring';
/**

View File

@ -31,7 +31,7 @@ import {findAuthorID} from "./SessionManager";
import {editOnly, loadTest, requireAuthentication, requireSession} from "../utils/Settings";
import webaccess from "../hooks/express/webaccess";
import {normalizeAuthzLevel} from "../hooks/express/webaccess";
import log4js from "log4js";
@ -92,7 +92,7 @@ export const checkAccess = async (padID, sessionCookie, token, userSettings) =>
// Note: userSettings.padAuthorizations should still be populated even if
// settings.requireAuthorization is false.
const padAuthzs = userSettings.padAuthorizations || {};
const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]);
const level = normalizeAuthzLevel(padAuthzs[padID]);
if (!level) {
authLogger.debug('access denied: unauthorized');
return DENY;

View File

@ -19,12 +19,12 @@
* limitations under the License.
*/
import absolutePaths from '../utils/AbsolutePaths';
import {makeAbsolute} from '../utils/AbsolutePaths';
import fs from 'fs';
import * as api from '../db/API';
import log4js from 'log4js';
import {sanitizePadId} from '../db/PadManager';
import randomString from '../utils/randomstring';
import {randomString} from '../utils/randomstring';
const argv = require('../utils/Cli').argv;
const createHTTPError = require('http-errors');
@ -32,7 +32,7 @@ const apiHandlerLogger = log4js.getLogger('APIHandler');
// ensure we have an apikey
let apikey = null;
const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt');
const apikeyFilename = makeAbsolute(argv.apikey || './APIKEY.txt');
try {
apikey = fs.readFileSync(apikeyFilename, 'utf8');

View File

@ -29,8 +29,8 @@ import {promises as fs} from "fs";
import {abiword, allowUnknownFileEnds, importMaxFileSize, soffice} from '../utils/Settings';
import {Formidable} from 'formidable';
import os from 'os';
import importHtml from '../utils/ImportHtml';
import importEtherpad from '../utils/ImportEtherpad';
import {setPadHTML} from '../utils/ImportHtml';
import {setPadRaw} from '../utils/ImportEtherpad';
import log4js from 'log4js';
import hooks from '../../static/js/pluginfw/hooks.js';
@ -150,7 +150,7 @@ const doImport = async (req, res, padId, authorId) => {
}
const text = await fs.readFile(srcFile, 'utf8');
directDatabaseAccess = true;
await importEtherpad.setPadRaw(padId, text, authorId);
await setPadRaw(padId, text, authorId);
}
// convert file to html if necessary
@ -207,7 +207,7 @@ const doImport = async (req, res, padId, authorId) => {
if (!directDatabaseAccess) {
if (importHandledByPlugin || useConverter || fileIsHTML) {
try {
await importHtml.setPadHTML(pad, text, authorId);
await setPadHTML(pad, text, authorId);
} catch (err) {
logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`);
}

View File

@ -58,7 +58,7 @@ import {createCollection} from '../stats';
import {strict as assert} from "assert";
import {RateLimiterMemory} from 'rate-limiter-flexible';
import webaccess from '../hooks/express/webaccess';
import {userCanModify} from '../hooks/express/webaccess';
import {ErrorCaused} from "../models/ErrorCaused";
import {Pad} from "../db/Pad";
import {SessionInfo} from "../models/SessionInfo";
@ -164,7 +164,7 @@ const padChannels = new Channels((ch, {socket, message}) => handleUserChanges(so
* This Method is called by server.js to tell the message handler on which socket it should send
* @param socket_io The Socket
*/
exports.setSocketIO = (socket_io) => {
export const setSocketIO = (socket_io) => {
socketio = socket_io;
};
@ -172,7 +172,7 @@ exports.setSocketIO = (socket_io) => {
* Handles the connection of a new user
* @param socket the socket.io Socket object for the new connection from the client
*/
exports.handleConnect = (socket) => {
export const handleConnect = (socket) => {
createCollection.meter('connects').mark();
// Initialize sessioninfos for this new session
@ -186,7 +186,7 @@ exports.handleConnect = (socket) => {
/**
* Kicks all sessions from a pad
*/
exports.kickSessionsFromPad = (padID) => {
export const kickSessionsFromPad = (padID) => {
if (typeof socketio.sockets.clients !== 'function') return;
// skip if there is nobody on this pad
@ -200,7 +200,7 @@ exports.kickSessionsFromPad = (padID) => {
* Handles the disconnection of a user
* @param socket the socket.io Socket object for the client
*/
exports.handleDisconnect = async (socket) => {
export const handleDisconnect = async (socket) => {
createCollection.meter('disconnects').mark();
const session = sessioninfos[socket.id];
delete sessioninfos[socket.id];
@ -238,7 +238,7 @@ exports.handleDisconnect = async (socket) => {
* @param socket the socket.io Socket object for the client
* @param message the message from the client
*/
exports.handleMessage = async (socket, message) => {
export const handleMessage = async (socket, message) => {
const env = process.env.NODE_ENV || 'development';
if (env === 'production') {
@ -272,7 +272,7 @@ exports.handleMessage = async (socket, message) => {
thisSession.padId = padIds.padId;
thisSession.readOnlyPadId = padIds.readOnlyPadId;
thisSession.readonly =
padIds.readonly || !webaccess.userCanModify(thisSession.auth.padID, socket.client.request);
padIds.readonly || !userCanModify(thisSession.auth.padID, socket.client.request);
}
// Outside of the checks done by this function, message.padId must not be accessed because it is
// too easy to introduce a security vulnerability that allows malicious users to read or modify
@ -416,7 +416,7 @@ const handleSaveRevisionMessage = async (socket, message) => {
* @param msg {Object} the message we're sending
* @param sessionID {string} the socketIO session to which we're sending this message
*/
exports.handleCustomObjectMessage = (msg, sessionID) => {
export const handleCustomObjectMessage = (msg, sessionID) => {
if (msg.data.type === 'CUSTOM') {
if (sessionID) {
// a sessionID is targeted: directly to this sessionID
@ -684,7 +684,7 @@ const handleUserChanges = async (socket, message) => {
socket.json.send({type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev}});
thisSession.rev = newRev;
if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev);
await exports.updatePadClients(pad);
await updatePadClients(pad);
} catch (err) {
socket.json.send({disconnect: 'badChangeset'});
createCollection.meter('failedChangesets').mark();

View File

@ -39,18 +39,18 @@ let io;
/**
* adds a component
*/
exports.addComponent = (moduleName, module) => {
if (module == null) return exports.deleteComponent(moduleName);
export const addComponent = (moduleName, module) => {
if (module == null) return deleteComponent(moduleName);
components[moduleName] = module;
module.setSocketIO(io);
};
exports.deleteComponent = (moduleName) => { delete components[moduleName]; };
export const deleteComponent = (moduleName) => { delete components[moduleName]; };
/**
* sets the socket.io and adds event functions for routing
*/
exports.setSocketIO = (_io) => {
export const setSocketIO = (_io) => {
io = _io;
io.sockets.on('connection', (socket) => {

View File

@ -1,34 +1,57 @@
'use strict';
const _ = require('underscore');
const cookieParser = require('cookie-parser');
const events = require('events');
const express = require('express');
const expressSession = require('express-session');
const fs = require('fs');
const hooks = require('../../static/js/pluginfw/hooks');
const log4js = require('log4js');
const SessionStore = require('../db/SessionStore');
const settings = require('../utils/Settings');
const stats = require('../stats');
const util = require('util');
const webaccess = require('./express/webaccess');
import _ from 'underscore';
import cookieParser from 'cookie-parser';
import events from "events";
import express from "express";
import fs from "fs";
import expressSession from "express-session";
import hooks from "../../static/js/pluginfw/hooks";
import log4js from "log4js";
import SessionStore from "../db/SessionStore";
import {
cookie,
exposeVersion,
getEpVersion,
getGitCommit,
ip,
loglevel,
port,
sessionKey,
ssl, sslKeys,
trustProxy,
users
} from "../utils/Settings";
import {createCollection} from "../stats";
import util from "util";
import {checkAccess, checkAccess2} from "./express/webaccess";
import {Socket} from "net";
const logger = log4js.getLogger('http');
let serverName;
let sessionStore;
const sockets = new Set();
const sockets = new Set<Socket>();
const socketsEvents = new events.EventEmitter();
const startTime = stats.settableGauge('httpStartTime');
exports.server = null;
const startTime = createCollection.settableGauge('httpStartTime');
export let server = null;
export let sessionMiddleware;
const closeServer = async () => {
if (exports.server != null) {
if (server != null) {
logger.info('Closing HTTP server...');
// Call exports.server.close() to reject new connections but don't await just yet because the
// Promise won't resolve until all preexisting connections are closed.
const p = util.promisify(exports.server.close.bind(exports.server))();
const p = util.promisify(server.close.bind(server))();
await hooks.aCallAll('expressCloseServer');
// Give existing connections some time to close on their own before forcibly terminating. The
// time should be long enough to avoid interrupting most preexisting transmissions but short
@ -47,7 +70,7 @@ const closeServer = async () => {
}
await p;
clearTimeout(timeout);
exports.server = null;
server = null;
startTime.setValue(0);
logger.info('HTTP server closed');
}
@ -55,24 +78,24 @@ const closeServer = async () => {
sessionStore = null;
};
exports.createServer = async () => {
export const createServer = async () => {
console.log('Report bugs at https://github.com/ether/etherpad-lite/issues');
serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`;
serverName = `Etherpad ${getGitCommit()} (https://etherpad.org)`;
console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`);
console.log(`Your Etherpad version is ${getEpVersion()} (${getGitCommit()})`);
await exports.restartServer();
await restartServer();
if (settings.ip === '') {
if (ip.length===0) {
// using Unix socket for connectivity
console.log(`You can access your Etherpad instance using the Unix socket at ${settings.port}`);
console.log(`You can access your Etherpad instance using the Unix socket at ${port}`);
} else {
console.log(`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`);
console.log(`You can access your Etherpad instance at http://${ip}:${port}/`);
}
if (!_.isEmpty(settings.users)) {
console.log(`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`);
if (!_.isEmpty(users)) {
console.log(`The plugin admin page is at http://${ip}:${port}/admin/plugins`);
} else {
console.warn('Admin username and password not set in settings.json. ' +
'To access admin please uncomment and edit "users" in settings.json');
@ -87,39 +110,40 @@ exports.createServer = async () => {
}
};
exports.restartServer = async () => {
export const restartServer = async () => {
await closeServer();
const app = express(); // New syntax for express v3
if (settings.ssl) {
if (ssl) {
console.log('SSL -- enabled');
console.log(`SSL -- server key file: ${settings.ssl.key}`);
console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`);
console.log(`SSL -- server key file: ${sslKeys.key}`);
console.log(`SSL -- Certificate Authority's certificate file: ${sslKeys.cert}`);
const options = {
key: fs.readFileSync(settings.ssl.key),
cert: fs.readFileSync(settings.ssl.cert),
key: fs.readFileSync(sslKeys.key),
cert: fs.readFileSync(sslKeys.cert),
ca: undefined
};
if (settings.ssl.ca) {
if (sslKeys.ca) {
options.ca = [];
for (let i = 0; i < settings.ssl.ca.length; i++) {
const caFileName = settings.ssl.ca[i];
for (let i = 0; i < sslKeys.ca.length; i++) {
const caFileName = sslKeys.ca[i];
options.ca.push(fs.readFileSync(caFileName));
}
}
const https = require('https');
exports.server = https.createServer(options, app);
server = https.createServer(options, app);
} else {
const http = require('http');
exports.server = http.createServer(app);
server = http.createServer(app);
}
app.use((req, res, next) => {
// res.header("X-Frame-Options", "deny"); // breaks embedded pads
if (settings.ssl) {
if (ssl) {
// we use SSL
res.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
@ -138,14 +162,14 @@ exports.restartServer = async () => {
res.header('Referrer-Policy', 'same-origin');
// send git version in the Server response header if exposeVersion is true.
if (settings.exposeVersion) {
if (exposeVersion) {
res.header('Server', serverName);
}
next();
});
if (settings.trustProxy) {
if (trustProxy) {
/*
* If 'trust proxy' === true, the clients IP address in req.ip will be the
* left-most entry in the X-Forwarded-* header.
@ -157,8 +181,10 @@ exports.restartServer = async () => {
// Measure response time
app.use((req, res, next) => {
const stopWatch = stats.timer('httpRequests').start();
const stopWatch = createCollection.timer('httpRequests').start();
const sendFn = res.send.bind(res);
// FIXME Check if this is still needed
// @ts-ignore
res.send = (...args) => { stopWatch.end(); sendFn(...args); };
next();
});
@ -167,20 +193,20 @@ exports.restartServer = async () => {
// starts listening to requests as reported in issue #158. Not installing the log4js connect
// logger when the log level has a higher severity than INFO since it would not log at that level
// anyway.
if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) {
if (!(loglevel === 'WARN') && loglevel === 'ERROR') {
app.use(log4js.connectLogger(logger, {
level: log4js.levels.DEBUG,
level: loglevel,
format: ':status, :method :url',
}));
}
app.use(cookieParser(settings.sessionKey, {}));
app.use(cookieParser(sessionKey, {}));
sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval);
exports.sessionMiddleware = expressSession({
sessionStore = new SessionStore(cookie.sessionRefreshInterval);
sessionMiddleware = expressSession({
propagateTouch: true,
rolling: true,
secret: settings.sessionKey,
secret: sessionKey,
store: sessionStore,
resave: false,
saveUninitialized: false,
@ -188,8 +214,8 @@ exports.restartServer = async () => {
// cleaner :)
name: 'express_sid',
cookie: {
maxAge: settings.cookie.sessionLifetime || null, // Convert 0 to null.
sameSite: settings.cookie.sameSite,
maxAge: cookie.sessionLifetime || null, // Convert 0 to null.
sameSite: cookie.sameSite,
// The automatic express-session mechanism for determining if the application is being served
// over ssl is similar to the one used for setting the language cookie, which check if one of
@ -215,15 +241,15 @@ exports.restartServer = async () => {
// middleware. This allows plugins to avoid creating an express-session record in the database
// when it is not needed (e.g., public static content).
await hooks.aCallAll('expressPreSession', {app});
app.use(exports.sessionMiddleware);
app.use(sessionMiddleware);
app.use(webaccess.checkAccess);
app.use(checkAccess2);
await Promise.all([
hooks.aCallAll('expressConfigure', {app}),
hooks.aCallAll('expressCreateServer', {app, server: exports.server}),
hooks.aCallAll('expressCreateServer', {app, server: server}),
]);
exports.server.on('connection', (socket) => {
server.on('connection', (socket) => {
sockets.add(socket);
socketsEvents.emit('updated');
socket.on('close', () => {
@ -231,11 +257,11 @@ exports.restartServer = async () => {
socketsEvents.emit('updated');
});
});
await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip);
await util.promisify(server.listen).bind(server)(port, ip);
startTime.setValue(Date.now());
logger.info('HTTP server listening for connections');
};
exports.shutdown = async (hookName, context) => {
export const shutdown = async (hookName, context) => {
await closeServer();
};

View File

@ -8,7 +8,7 @@ import {doesPadExist} from '../../db/PadManager';
import {getPadId, isReadOnlyId} from '../../db/ReadOnlyManager';
import rateLimit from 'express-rate-limit';
import {checkAccess} from '../../db/SecurityManager';
import webaccess from './webaccess';
import {userCanModify} from './webaccess';
exports.expressCreateServer = (hookName, args, cb) => {
importExportRateLimiting.onLimitReached = (req, res, options) => {
@ -72,7 +72,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
const {session: {user} = {}}:SessionSocketModel = req;
const {accessStatus, authorID: authorId} = await checkAccess(
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) {
if (accessStatus !== 'grant' || !userCanModify(req.params.pad, req)) {
return res.status(403).send('Forbidden');
}
await doImport2(req, res, req.params.pad, authorId);

View File

@ -1,18 +1,18 @@
'use strict';
const padManager = require('../../db/PadManager');
import {isValidPadId, sanitizePadId} from '../../db/PadManager';
exports.expressCreateServer = (hookName, args, cb) => {
// redirects browser to the pad's sanitized url if needed. otherwise, renders the html
args.app.param('pad', (req, res, next, padId) => {
(async () => {
// ensure the padname is valid and the url doesn't end with a /
if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) {
if (!isValidPadId(padId) || /\/$/.test(req.url)) {
res.status(404).send('Such a padname is forbidden');
return;
}
const sanitizedPadId = await padManager.sanitizePadId(padId);
const sanitizedPadId = await sanitizePadId(padId);
if (sanitizedPadId === padId) {
// the pad id was fine, so just render it

View File

@ -1,14 +1,14 @@
'use strict';
const events = require('events');
const express = require('../express');
const log4js = require('log4js');
const proxyaddr = require('proxy-addr');
const settings = require('../../utils/Settings');
const socketio = require('socket.io');
const socketIORouter = require('../../handler/SocketIORouter');
const hooks = require('../../../static/js/pluginfw/hooks');
const padMessageHandler = require('../../handler/PadMessageHandler');
import events from 'events';
import {sessionMiddleware} from '../express';
import log4js from 'log4js';
import proxyaddr from 'proxy-addr';
import {socketIo, socketTransportProtocols, trustProxy} from '../../utils/Settings';
import socketio from 'socket.io';
import {addComponent, setSocketIO} from '../../handler/SocketIORouter';
import hooks from '../../../static/js/pluginfw/hooks';
import * as padMessageHandler from '../../handler/PadMessageHandler';
let io;
const logger = log4js.getLogger('socket.io');
@ -52,7 +52,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
// transports in this list at once
// e.g. XHR is disabled in IE by default, so in IE it should use jsonp-polling
io = socketio({
transports: settings.socketTransportProtocols,
transports: socketTransportProtocols,
}).listen(args.server, {
/*
* Do not set the "io" cookie.
@ -74,7 +74,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
* https://github.com/socketio/socket.io/issues/2276#issuecomment-147184662 (not totally true, actually, see above)
*/
cookie: false,
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
maxHttpBufferSize: socketIo.maxHttpBufferSize,
});
io.on('connect', (socket) => {
@ -90,7 +90,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
const req = socket.request;
// Express sets req.ip but socket.io does not. Replicate Express's behavior here.
if (req.ip == null) {
if (settings.trustProxy) {
if (trustProxy) {
req.ip = proxyaddr(req, args.app.get('trust proxy fn'));
} else {
req.ip = socket.handshake.address;
@ -102,7 +102,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
req.headers.cookie = socket.handshake.query.cookie;
}
// See: https://socket.io/docs/faq/#Usage-with-express-session
express.sessionMiddleware(req, {}, next);
sessionMiddleware(req, {}, next);
});
io.use((socket, next) => {
@ -130,8 +130,8 @@ exports.expressCreateServer = (hookName, args, cb) => {
// if(settings.minify) io.enable('browser client minification');
// Initialize the Socket.IO Router
socketIORouter.setSocketIO(io);
socketIORouter.addComponent('pad', padMessageHandler);
setSocketIO(io);
addComponent('pad', padMessageHandler);
hooks.callAll('socketio', {app: args.app, io, server: args.server});

View File

@ -1,14 +1,14 @@
'use strict';
const path = require('path');
const eejs = require('../../eejs');
const fs = require('fs');
import path from 'path';
import {required} from '../../eejs';
import fs from 'fs';
const fsp = fs.promises;
const toolbar = require('../../utils/toolbar');
const hooks = require('../../../static/js/pluginfw/hooks');
const settings = require('../../utils/Settings');
const util = require('util');
const webaccess = require('./webaccess');
import {} from '../../utils/toolbar';
import hooks from '../../../static/js/pluginfw/hooks';
import {favicon, getEpVersion, maxAge, root, skinName} from '../../utils/Settings';
import util from 'util';
import {userCanModify} from './webaccess';
exports.expressPreSession = async (hookName, {app}) => {
// This endpoint is intended to conform to:
@ -17,25 +17,25 @@ exports.expressPreSession = async (hookName, {app}) => {
res.set('Content-Type', 'application/health+json');
res.json({
status: 'pass',
releaseId: settings.getEpVersion(),
releaseId: getEpVersion(),
});
});
app.get('/stats', (req, res) => {
res.json(require('../../stats').toJSON());
res.json(required('../../stats').toJSON());
});
app.get('/javascript', (req, res) => {
res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req}));
res.send(required('ep_etherpad-lite/templates/javascript.html', {req}));
});
app.get('/robots.txt', (req, res) => {
let filePath =
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt');
path.join(root, 'src', 'static', 'skins', skinName, 'robots.txt');
res.sendFile(filePath, (err) => {
// there is no custom robots.txt, send the default robots.txt which dissallows all
if (err) {
filePath = path.join(settings.root, 'src', 'static', 'robots.txt');
filePath = path.join(root, 'src', 'static', 'robots.txt');
res.sendFile(filePath);
}
});
@ -44,9 +44,9 @@ exports.expressPreSession = async (hookName, {app}) => {
app.get('/favicon.ico', (req, res, next) => {
(async () => {
const fns = [
...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []),
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'),
path.join(settings.root, 'src', 'static', 'favicon.ico'),
...(favicon ? [path.resolve(root, favicon)] : []),
path.join(root, 'src', 'static', 'skins', skinName, 'favicon.ico'),
path.join(root, 'src', 'static', 'favicon.ico'),
];
for (const fn of fns) {
try {
@ -54,7 +54,7 @@ exports.expressPreSession = async (hookName, {app}) => {
} catch (err) {
continue;
}
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
await util.promisify(res.sendFile.bind(res))(fn);
return;
}
@ -66,13 +66,13 @@ exports.expressPreSession = async (hookName, {app}) => {
exports.expressCreateServer = (hookName, args, cb) => {
// serve index.html under /
args.app.get('/', (req, res) => {
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req}));
res.send(required('ep_etherpad-lite/templates/index.html', {req}));
});
// serve pad.html under /p
args.app.get('/p/:pad', (req, res, next) => {
// The below might break for pads being rewritten
const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
const isReadOnly = !userCanModify(req.params.pad, req);
hooks.callAll('padInitToolbar', {
toolbar,
@ -81,7 +81,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
// can be removed when require-kernel is dropped
res.header('Feature-Policy', 'sync-xhr \'self\'');
res.send(eejs.require('ep_etherpad-lite/templates/pad.html', {
res.send(required('ep_etherpad-lite/templates/pad.html', {
req,
toolbar,
isReadOnly,
@ -94,7 +94,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
toolbar,
});
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
res.send(required('ep_etherpad-lite/templates/timeslider.html', {
req,
toolbar,
}));

View File

@ -19,8 +19,9 @@ const getTar = async () => {
};
const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8');
const tar = {};
for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) {
const files = relativeFiles.map(prefixLocalLibraryPath);
for (let [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) {
const relativeFilesMapped = relativeFiles as string[];
const files = relativeFilesMapped.map(prefixLocalLibraryPath);
tar[prefixLocalLibraryPath(key)] = files
.concat(files.map((p) => p.replace(/\.js$/, '')))
.concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`));
@ -28,7 +29,7 @@ const getTar = async () => {
return tar;
};
exports.expressPreSession = async (hookName, {app}) => {
export const expressPreSession = async (hookName, {app}) => {
// Cache both minified and static.
const assetCache = new CachingMiddleware();
app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache));
@ -62,8 +63,9 @@ exports.expressPreSession = async (hookName, {app}) => {
const clientParts = plugins.parts.filter((part) => part.client_hooks != null);
const clientPlugins = {};
for (const name of new Set(clientParts.map((part) => part.plugin))) {
clientPlugins[name] = {...plugins.plugins[name]};
delete clientPlugins[name].package;
const mappedName = name as any
clientPlugins[mappedName] = {...plugins.plugins[mappedName]};
delete clientPlugins[mappedName].package;
}
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);

View File

@ -1,11 +1,14 @@
'use strict';
const path = require('path');
const fsp = require('fs').promises;
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const sanitizePathname = require('../../utils/sanitizePathname');
const settings = require('../../utils/Settings');
import path from 'path';
import {promises as fsp} from "fs";
import plugins from "../../../static/js/pluginfw/plugin_defs";
import sanitizePathname from "../../utils/sanitizePathname";
import {enableAdminUITests, root} from "../../utils/Settings";
import {Presession} from "../../models/Presession";
// Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/'
// instead of path.sep to separate pathname components.
const findSpecs = async (specDir) => {
@ -29,16 +32,17 @@ const findSpecs = async (specDir) => {
return specs;
};
exports.expressPreSession = async (hookName, {app}) => {
export const expressPreSession = async (hookName, {app}) => {
app.get('/tests/frontend/frontendTestSpecs.json', (req, res, next) => {
(async () => {
const modules = [];
await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => {
let {package: {path: pluginPath}} = def;
const mappedDef = def as Presession;
let {package: {path: pluginPath}} = mappedDef;
if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep;
const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`;
for (const spec of await findSpecs(path.join(pluginPath, specDir))) {
if (plugin === 'ep_etherpad-lite' && !settings.enableAdminUITests &&
if (plugin === 'ep_etherpad-lite' && !enableAdminUITests &&
spec.startsWith('admin')) continue;
modules.push(`${plugin}/${specDir}/${spec.replace(/\.js$/, '')}`);
}
@ -57,7 +61,7 @@ exports.expressPreSession = async (hookName, {app}) => {
})().catch((err) => next(err || new Error(err)));
});
const rootTestFolder = path.join(settings.root, 'src/tests/frontend/');
const rootTestFolder = path.join(root, 'src/tests/frontend/');
app.get('/tests/frontend/index.html', (req, res) => {
res.redirect(['./', ...req.url.split('?').slice(1)].join('?'));

View File

@ -1,12 +1,17 @@
'use strict';
const assert = require('assert').strict;
const log4js = require('log4js');
const httpLogger = log4js.getLogger('http');
const settings = require('../../utils/Settings');
const hooks = require('../../../static/js/pluginfw/hooks');
const readOnlyManager = require('../../db/ReadOnlyManager');
import {strict as assert} from "assert";
import log4js from "log4js";
import {requireAuthentication, requireAuthorization, setUsers, users} from "../../utils/Settings";
import hooks from "../../../static/js/pluginfw/hooks";
import {getPadId, isReadOnlyId} from "../../db/ReadOnlyManager";
import {UserIndexedModel} from "../../models/UserIndexedModel";
const httpLogger = log4js.getLogger('http');
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
// Promisified wrapper around hooks.aCallFirst.
@ -17,7 +22,7 @@ const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, rej
const aCallFirst0 =
async (hookName, context, pred = null) => (await aCallFirst(hookName, context, pred))[0];
exports.normalizeAuthzLevel = (level) => {
export const normalizeAuthzLevel = (level) => {
if (!level) return false;
switch (level) {
case true:
@ -32,20 +37,20 @@ exports.normalizeAuthzLevel = (level) => {
return false;
};
exports.userCanModify = (padId, req) => {
if (readOnlyManager.isReadOnlyId(padId)) return false;
if (!settings.requireAuthentication) return true;
const {session: {user} = {}} = req;
export const userCanModify = (padId, req) => {
if (isReadOnlyId(padId)) return false;
if (!requireAuthentication) return true;
const {session: {user} = {}}:SessionSocketModel = req;
if (!user || user.readOnly) return false;
assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization.
const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]);
const level = normalizeAuthzLevel(user.padAuthorizations[padId]);
return level && level !== 'readOnly';
};
// Exported so that tests can set this to 0 to avoid unnecessary test slowness.
exports.authnFailureDelayMs = 1000;
export const authnFailureDelayMs = 1000;
const checkAccess = async (req, res, next) => {
export const checkAccess = async (req, res, next) => {
const requireAdmin = req.path.toLowerCase().startsWith('/admin');
// ///////////////////////////////////////////////////////////////////////////////////////////////
@ -88,16 +93,16 @@ const checkAccess = async (req, res, next) => {
// authentication is checked and once after (if settings.requireAuthorization is true).
const authorize = async () => {
const grant = async (level) => {
level = exports.normalizeAuthzLevel(level);
level = normalizeAuthzLevel(level);
if (!level) return false;
const user = req.session.user;
if (user == null) return true; // This will happen if authentication is not required.
const encodedPadId = (req.path.match(/^\/p\/([^/]*)/) || [])[1];
if (encodedPadId == null) return true;
let padId = decodeURIComponent(encodedPadId);
if (readOnlyManager.isReadOnlyId(padId)) {
if (isReadOnlyId(padId)) {
// pad is read-only, first get the real pad ID
padId = await readOnlyManager.getPadId(padId);
padId = await getPadId(padId);
if (padId == null) return false;
}
// The user was granted access to a pad. Remember the authorization level in the user's
@ -108,11 +113,11 @@ const checkAccess = async (req, res, next) => {
};
const isAuthenticated = req.session && req.session.user;
if (isAuthenticated && req.session.user.is_admin) return await grant('create');
const requireAuthn = requireAdmin || settings.requireAuthentication;
const requireAuthn = requireAdmin || requireAuthentication;
if (!requireAuthn) return await grant('create');
if (!isAuthenticated) return await grant(false);
if (requireAdmin && !req.session.user.is_admin) return await grant(false);
if (!settings.requireAuthorization) return await grant('create');
if (!requireAuthorization) return await grant('create');
return await grant(await aCallFirst0('authorize', {req, res, next, resource: req.path}));
};
@ -131,8 +136,10 @@ const checkAccess = async (req, res, next) => {
// page).
// ///////////////////////////////////////////////////////////////////////////////////////////////
if (settings.users == null) settings.users = {};
const ctx = {req, res, users: settings.users, next};
if (users == null) setUsers({});
const ctx = {req, res, users: users, next, username: undefined,
password: undefined
};
// If the HTTP basic auth header is present, extract the username and password so it can be given
// to authn plugins.
const httpBasicAuth = req.headers.authorization && req.headers.authorization.startsWith('Basic ');
@ -148,7 +155,7 @@ const checkAccess = async (req, res, next) => {
}
if (!(await aCallFirst0('authenticate', ctx))) {
// Fall back to HTTP basic auth.
const {[ctx.username]: {password} = {}} = settings.users;
const {[ctx.username]: {password} = {password: undefined}}:UserIndexedModel = users;
if (!httpBasicAuth || !ctx.username || password == null || password !== ctx.password) {
httpLogger.info(`Failed authentication from IP ${req.ip}`);
if (await aCallFirst0('authnFailure', {req, res})) return;
@ -156,14 +163,14 @@ const checkAccess = async (req, res, next) => {
// No plugin handled the authentication failure. Fall back to basic authentication.
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
// Delay the error response for 1s to slow down brute force attacks.
await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs));
await new Promise((resolve) => setTimeout(resolve, authnFailureDelayMs));
res.status(401).send('Authentication Required');
return;
}
settings.users[ctx.username].username = ctx.username;
users[ctx.username].username = ctx.username;
// Make a shallow copy so that the password property can be deleted (to prevent it from
// appearing in logs or in the database) without breaking future authentication attempts.
req.session.user = {...settings.users[ctx.username]};
req.session.user = {...users[ctx.username]};
delete req.session.user.password;
}
if (req.session.user == null) {
@ -190,6 +197,7 @@ const checkAccess = async (req, res, next) => {
* Express middleware to authenticate the user and check authorization. Must be installed after the
* express-session middleware.
*/
exports.checkAccess = (req, res, next) => {
// FIXME why same method twice?
export const checkAccess2 = (req, res, next) => {
checkAccess(req, res, next).catch((err) => next(err || new Error(err)));
};

View File

@ -1,12 +1,13 @@
'use strict';
const languages = require('languages4translatewiki');
const fs = require('fs');
const path = require('path');
const _ = require('underscore');
const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js');
const existsSync = require('../utils/path_exists');
const settings = require('../utils/Settings');
import languages from 'languages4translatewiki';
import fs from 'fs';
import path from 'path';
import _ from 'underscore';
import pluginDefs from '../../static/js/pluginfw/plugin_defs.js';
import {check} from '../utils/path_exists';
import {customLocaleStrings, maxAge, root} from '../utils/Settings';
import {Presession} from "../models/Presession";
// returns all existing messages merged together and grouped by langcode
// {es: {"foo": "string"}, en:...}
@ -17,7 +18,7 @@ const getAllLocales = () => {
// into `locales2paths` (files from various dirs are grouped by lang code)
// (only json files with valid language code as name)
const extractLangs = (dir) => {
if (!existsSync(dir)) return;
if (!check(dir)) return;
let stat = fs.lstatSync(dir);
if (!stat.isDirectory() || stat.isSymbolicLink()) return;
@ -37,13 +38,14 @@ const getAllLocales = () => {
};
// add core supported languages first
extractLangs(path.join(settings.root, 'src/locales'));
extractLangs(path.join(root, 'src/locales'));
// add plugins languages (if any)
for (const {package: {path: pluginPath}} of Object.values(pluginDefs.plugins)) {
for (const val of Object.values(pluginDefs.plugins)) {
const pluginPath:Presession = val as Presession
// plugin locales should overwrite etherpad's core locales
if (pluginPath.endsWith('/ep_etherpad-lite') === true) continue;
extractLangs(path.join(pluginPath, 'locales'));
if (pluginPath.package.path.endsWith('/ep_etherpad-lite') === true) continue;
extractLangs(path.join(pluginPath.package.path, 'locales'));
}
// Build a locale index (merge all locale data other than user-supplied overrides)
@ -68,9 +70,9 @@ const getAllLocales = () => {
const wrongFormatErr = Error(
'customLocaleStrings in wrong format. See documentation ' +
'for Customization for Administrators, under Localization.');
if (settings.customLocaleStrings) {
if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr;
_.each(settings.customLocaleStrings, (overrides, langcode) => {
if (customLocaleStrings) {
if (typeof customLocaleStrings !== 'object') throw wrongFormatErr;
_.each(customLocaleStrings, (overrides, langcode) => {
if (typeof overrides !== 'object') throw wrongFormatErr;
_.each(overrides, (localeString, key) => {
if (typeof localeString !== 'string') throw wrongFormatErr;
@ -112,7 +114,7 @@ exports.expressPreSession = async (hookName, {app}) => {
// works with /locale/en and /locale/en.json requests
const locale = req.params.locale.split('.')[0];
if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) {
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`);
} else {
@ -121,7 +123,7 @@ exports.expressPreSession = async (hookName, {app}) => {
});
app.get('/locales.json', (req, res) => {
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.send(localeIndex);
});

View File

@ -0,0 +1,11 @@
export type CMDOptions = {
cwd?: string,
stdio?: string|any[],
env?: NodeJS.ProcessEnv
}
export type CMDPromise = {
stdout: string,
stderr: string,
child: any
}

View File

@ -0,0 +1 @@
export type LogLevel = "DEBUG"|"INFO"|"WARN"|"ERROR"

View File

@ -0,0 +1,3 @@
export type PadDiffModel = {
_authors: any[]
}

View File

@ -0,0 +1,5 @@
export type Presession = {
package:{
path: string,
}
}

View File

@ -3,6 +3,8 @@ type SessionSocketModel = {
user?: {
username?: string,
is_admin?: boolean
readOnly?: boolean,
padAuthorizations?: any
}
}
}

View File

@ -0,0 +1,5 @@
export type UserIndexedModel = {
[key: string]: {
password?: string|undefined,
}
}

View File

@ -203,11 +203,13 @@ export const stop = async () => {
} catch (err) {
logger.error('Error occurred while stopping Etherpad');
state = State.STATE_TRANSITION_FAILED;
// @ts-ignore
stopDoneGate.resolve();
return await exit(err);
}
logger.info('Etherpad stopped');
state = State.STOPPED;
// @ts-ignore
stopDoneGate.resolve();
};

View File

@ -19,20 +19,21 @@
* limitations under the License.
*/
const spawn = require('child_process').spawn;
const async = require('async');
const settings = require('./Settings');
const os = require('os');
import {spawn} from "child_process";
import async from 'async';
import {abiword} from './Settings';
import os from 'os';
// on windows we have to spawn a process for each convertion,
// cause the plugin abicommand doesn't exist on this platform
if (os.type().indexOf('Windows') > -1) {
exports.convertFile = async (srcFile, destFile, type) => {
const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]);
const abiword2 = spawn(abiword, [`--to=${destFile}`, srcFile]);
let stdoutBuffer = '';
abiword.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
await new Promise((resolve, reject) => {
abiword2.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
abiword2.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
await new Promise<void>((resolve, reject) => {
abiword.on('exit', (code) => {
if (code !== 0) return reject(new Error(`Abiword died with exit code ${code}`));
if (stdoutBuffer !== '') {
@ -46,14 +47,14 @@ if (os.type().indexOf('Windows') > -1) {
// communicate with it via stdin/stdout
// thats much faster, about factor 10
} else {
let abiword;
let abiword2;
let stdoutCallback = null;
const spawnAbiword = () => {
abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']);
abiword2 = spawn(abiword, ['--plugin', 'AbiCommand']);
let stdoutBuffer = '';
let firstPrompt = true;
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
abiword.on('exit', (code) => {
abiword2.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
abiword2.on('exit', (code) => {
spawnAbiword();
if (stdoutCallback != null) {
stdoutCallback(new Error(`Abiword died with exit code ${code}`));

View File

@ -19,9 +19,10 @@
* limitations under the License.
*/
const log4js = require('log4js');
const path = require('path');
const _ = require('underscore');
import log4js from 'log4js';
import path from "path";
import _ from "underscore";
const absPathLogger = log4js.getLogger('AbsolutePaths');
@ -75,7 +76,7 @@ const popIfEndsWith = (stringArray, lastDesiredElements) => {
* @return {string} The identified absolute base path. If such path cannot be
* identified, prints a log and exits the application.
*/
exports.findEtherpadRoot = () => {
export const findEtherpadRoot = () => {
if (etherpadRoot != null) {
return etherpadRoot;
}
@ -131,12 +132,12 @@ exports.findEtherpadRoot = () => {
* it is returned unchanged. Otherwise it is interpreted
* relative to exports.root.
*/
exports.makeAbsolute = (somePath) => {
export const makeAbsolute = (somePath) => {
if (path.isAbsolute(somePath)) {
return somePath;
}
const rewrittenPath = path.join(exports.findEtherpadRoot(), somePath);
const rewrittenPath = path.join(findEtherpadRoot(), somePath);
absPathLogger.debug(`Relative path "${somePath}" can be rewritten to "${rewrittenPath}"`);
return rewrittenPath;
@ -150,7 +151,7 @@ exports.makeAbsolute = (somePath) => {
* a subdirectory of the base one
* @return {boolean}
*/
exports.isSubdir = (parent, arbitraryDir) => {
export const isSubdir = (parent, arbitraryDir) => {
// modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825
const relative = path.relative(parent, arbitraryDir);
const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);

View File

@ -21,9 +21,8 @@
*/
// An object containing the parsed command-line options
exports.argv = {};
export const argv = process.argv.slice(2);
const argv = process.argv.slice(2);
let arg, prevArg;
// Loop through args

View File

@ -14,17 +14,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Stream} from "./Stream";
const Stream = require('./Stream');
const assert = require('assert').strict;
const authorManager = require('../db/AuthorManager');
const hooks = require('../../static/js/pluginfw/hooks');
const padManager = require('../db/PadManager');
import {strict as assert} from "assert";
exports.getPadRaw = async (padId, readOnlyId) => {
import {getAuthor} from "../db/AuthorManager";
import hooks from "../../static/js/pluginfw/hooks";
import {getPad} from "../db/PadManager";
export const getPadRaw = async (padId, readOnlyId) => {
const dstPfx = `pad:${readOnlyId || padId}`;
const [pad, customPrefixes] = await Promise.all([
padManager.getPad(padId),
getPad(padId),
hooks.aCallAll('exportEtherpadAdditionalContent'),
]);
const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix) => {
@ -43,7 +46,7 @@ exports.getPadRaw = async (padId, readOnlyId) => {
const records = (function* () {
for (const authorId of pad.getAllAuthors()) {
yield [`globalAuthor:${authorId}`, (async () => {
const authorEntry = await authorManager.getAuthor(authorId);
const authorEntry = await getAuthor(authorId);
if (!authorEntry) return undefined; // Becomes unset when converted to JSON.
if (authorEntry.padIDs) authorEntry.padIDs = readOnlyId || padId;
return authorEntry;

View File

@ -19,11 +19,10 @@
* limitations under the License.
*/
const AttributeMap = require('../../static/js/AttributeMap');
const Changeset = require('../../static/js/Changeset');
import AttributeMap from '../../static/js/AttributeMap';
import Changeset from '../../static/js/Changeset';
exports.getPadPlainText = (pad, revNum) => {
const _analyzeLine = exports._analyzeLine;
export const getPadPlainText = (pad, revNum) => {
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext);
const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
@ -45,8 +44,14 @@ exports.getPadPlainText = (pad, revNum) => {
};
exports._analyzeLine = (text, aline, apool) => {
const line = {};
export const _analyzeLine = (text, aline, apool) => {
const line = {
listLevel: undefined,
text: undefined,
listTypeName: undefined,
aline: undefined,
start: undefined
};
// identify list
let lineMarker = 0;
@ -81,5 +86,5 @@ exports._analyzeLine = (text, aline, apool) => {
};
exports._encodeWhitespace =
export const _encodeWhitespace =
(s) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`);

View File

@ -15,16 +15,19 @@
* limitations under the License.
*/
const Changeset = require('../../static/js/Changeset');
const attributes = require('../../static/js/attributes');
const padManager = require('../db/PadManager');
const _ = require('underscore');
const Security = require('../../static/js/security');
const hooks = require('../../static/js/pluginfw/hooks');
const eejs = require('../eejs');
const _analyzeLine = require('./ExportHelper')._analyzeLine;
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
const padutils = require('../../static/js/pad_utils').padutils;
import Changeset from '../../static/js/Changeset';
import attributes from "../../static/js/attributes";
import {getPad} from "../db/PadManager";
import _ from "underscore";
import Security from '../../static/js/security';
import hooks from '../../static/js/pluginfw/hooks';
import {required} from '../eejs';
import {_analyzeLine, _encodeWhitespace} from "./ExportHelper";
import {padutils} from "../../static/js/pad_utils";
const getPadHTML = async (pad, revNum) => {
let atext = pad.atext;
@ -38,7 +41,7 @@ const getPadHTML = async (pad, revNum) => {
return await getHTMLFromAtext(pad, atext);
};
const getHTMLFromAtext = async (pad, atext, authorColors) => {
export const getHTMLFromAtext = async (pad, atext, authorColors?) => {
const apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
@ -456,8 +459,8 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
return pieces.join('');
};
exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => {
const pad = await padManager.getPad(padId);
export const getPadHTMLDocument = async (padId, revNum, readOnlyId?) => {
const pad = await getPad(padId);
// Include some Styles into the Head for Export
let stylesForExportCSS = '';
@ -472,7 +475,7 @@ exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => {
html += hookHtml;
}
return eejs.require('ep_etherpad-lite/templates/export_html.html', {
return required('ep_etherpad-lite/templates/export_html.html', {
body: html,
padId: Security.escapeHTML(readOnlyId || padId),
extraCSS: stylesForExportCSS,
@ -525,7 +528,4 @@ const _processSpaces = (s) => {
}
}
return parts.join('');
};
exports.getPadHTML = getPadHTML;
exports.getHTMLFromAtext = getHTMLFromAtext;
}

View File

@ -39,7 +39,7 @@ const getPadTXT = async (pad, revNum) => {
// This is different than the functionality provided in ExportHtml as it provides formatting
// functionality that is designed specifically for TXT exports
const getTXTFromAtext = (pad, atext, authorColors) => {
export const getTXTFromAtext = (pad, atext, authorColors?) => {
const apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
@ -56,7 +56,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
});
const getLineTXT = (text, attribs) => {
const propVals = [false, false, false];
const propVals:(boolean|number)[] = [false, false, false];
const ENTER = 1;
const STAY = 2;
const LEAVE = 0;
@ -256,9 +256,8 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
return pieces.join('');
};
exports.getTXTFromAtext = getTXTFromAtext;
exports.getPadTXTDocument = async (padId, revNum) => {
export const getPadTXTDocument = async (padId, revNum) => {
const pad = await padManager.getPad(padId);
return getPadTXT(pad, revNum);
};

View File

@ -16,19 +16,21 @@
* limitations under the License.
*/
const AttributePool = require('../../static/js/AttributePool');
const {Pad} = require('../db/Pad');
const Stream = require('./Stream');
const authorManager = require('../db/AuthorManager');
const db = require('../db/DB');
const hooks = require('../../static/js/pluginfw/hooks');
const log4js = require('log4js');
const supportedElems = require('../../static/js/contentcollector').supportedElems;
const ueberdb = require('ueberdb2');
import {AttributePool} from '../../static/js/AttributePool';
import {Pad} from '../db/Pad';
import {Stream} from './Stream';
import {addPad, doesAuthorExist} from '../db/AuthorManager';
import {db} from '../db/DB';
import hooks from '../../static/js/pluginfw/hooks';
import log4js from "log4js";
import {supportedElems} from "../../static/js/contentcollector";
import ueberdb from 'ueberdb2';
const logger = log4js.getLogger('ImportEtherpad');
exports.setPadRaw = async (padId, r, authorId = '') => {
export const setPadRaw = async (padId, r, authorId = '') => {
const records = JSON.parse(r);
// get supported block Elements from plugins, we will use this later.
@ -69,7 +71,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => {
throw new TypeError('globalAuthor padIDs subkey is not a string');
}
checkOriginalPadId(value.padIDs);
if (await authorManager.doesAuthorExist(id)) {
if (await doesAuthorExist(id)) {
existingAuthors.add(id);
return;
}
@ -115,7 +117,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => {
const writeOps = (function* () {
for (const [k, v] of data) yield db.set(k, v);
for (const a of existingAuthors) yield authorManager.addPad(a, padId);
for (const a of existingAuthors) yield addPad(a as string, padId);
})();
for (const op of new Stream(writeOps).batch(100).buffer(99)) await op;
};

View File

@ -15,15 +15,15 @@
* limitations under the License.
*/
const log4js = require('log4js');
const Changeset = require('../../static/js/Changeset');
const contentcollector = require('../../static/js/contentcollector');
const jsdom = require('jsdom');
import log4js from 'log4js';
import Changeset from '../../static/js/Changeset';
import contentcollector from '../../static/js/contentcollector';
import jsdom from 'jsdom';
const apiLogger = log4js.getLogger('ImportHtml');
let processor;
exports.setPadHTML = async (pad, html, authorId = '') => {
export const setPadHTML = async (pad, html, authorId = '') => {
if (processor == null) {
const [{rehype}, {default: minifyWhitespace}] =
await Promise.all([import('rehype'), import('rehype-minify-whitespace')]);
@ -90,4 +90,4 @@ exports.setPadHTML = async (pad, html, authorId = '') => {
apiLogger.debug(`The changeset: ${theChangeset}`);
await pad.setText('\n', authorId);
await pad.appendRevision(theChangeset, authorId);
};
}

View File

@ -17,20 +17,23 @@
* limitations under the License.
*/
const async = require('async');
const fs = require('fs').promises;
const log4js = require('log4js');
const os = require('os');
const path = require('path');
const runCmd = require('./run_cmd');
const settings = require('./Settings');
import async from 'async';
import log4js from 'log4js';
import {promises as fs} from "fs";
import os from 'os';
import path from "path";
import {exportCMD} from "./run_cmd";
import {soffice} from "./Settings";
const logger = log4js.getLogger('LibreOffice');
const doConvertTask = async (task) => {
const tmpDir = os.tmpdir();
const p = runCmd([
settings.soffice,
const p = await exportCMD([
soffice,
'--headless',
'--invisible',
'--nologo',
@ -81,7 +84,7 @@ const queue = async.queue(doConvertTask, 1);
* @param {String} type The type to convert into
* @param {Function} callback Standard callback function
*/
exports.convertFile = async (srcFile, destFile, type) => {
export const convertFile = async (srcFile, destFile, type) => {
// Used for the moving of the file, not the conversion
const fileExtension = type;

View File

@ -21,21 +21,28 @@
* limitations under the License.
*/
const settings = require('./Settings');
const fs = require('fs').promises;
const path = require('path');
const plugins = require('../../static/js/pluginfw/plugin_defs');
const RequireKernel = require('etherpad-require-kernel');
const mime = require('mime-types');
const Threads = require('threads');
const log4js = require('log4js');
import {maxAge, root} from './Settings';
import {promises as fs} from "fs";
import path from "path";
import plugins from "../../static/js/pluginfw/plugin_defs";
import RequireKernel from "etherpad-require-kernel";
import mime from "mime-types";
import Threads from "threads";
import log4js from "log4js";
const sanitizePathname = require('./sanitizePathname');
const logger = log4js.getLogger('Minify');
const ROOT_DIR = path.join(settings.root, 'src/static/');
const ROOT_DIR = path.join(root, 'src/static/');
const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
const threadsPool = Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
const LIBRARY_WHITELIST = [
'async',
@ -89,7 +96,7 @@ const requestURI = async (url, method, headers) => {
return await p;
};
const requestURIs = (locations, method, headers, callback) => {
export const requestURIs = (locations, method, headers, callback) => {
Promise.all(locations.map(async (loc) => {
try {
return await requestURI(loc, method, headers);
@ -176,10 +183,10 @@ const minify = async (req, res) => {
date.setMilliseconds(0);
res.setHeader('last-modified', date.toUTCString());
res.setHeader('date', (new Date()).toUTCString());
if (settings.maxAge !== undefined) {
const expiresDate = new Date(Date.now() + settings.maxAge * 1000);
if (maxAge !== undefined) {
const expiresDate = new Date(Date.now() + maxAge * 1000);
res.setHeader('expires', expiresDate.toUTCString());
res.setHeader('cache-control', `max-age=${settings.maxAge}`);
res.setHeader('cache-control', `max-age=${maxAge}`);
}
}
@ -271,7 +278,7 @@ const requireDefinition = () => `var require = ${RequireKernel.kernelSource};\n`
const getFileCompressed = async (filename, contentType) => {
let content = await getFile(filename);
if (!content || !settings.minify) {
if (!content || !minify) {
return content;
} else if (contentType === 'application/javascript') {
return await new Promise((resolve) => {
@ -317,10 +324,8 @@ const getFile = async (filename) => {
return await fs.readFile(path.resolve(ROOT_DIR, filename));
};
exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));
export const minifyExp = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));
exports.requestURIs = requestURIs;
exports.shutdown = async (hookName, context) => {
export const shutdown = async (hookName, context) => {
await threadsPool.terminate();
};

View File

@ -3,11 +3,14 @@
* Worker thread to minify JS & CSS files out of the main NodeJS thread
*/
const CleanCSS = require('clean-css');
const Terser = require('terser');
const fsp = require('fs').promises;
const path = require('path');
const Threads = require('threads');
import CleanCSS from 'clean-css';
import Terser from "terser";
import {promises as fsp} from "fs";
import path from "path";
import Threads from "threads";
const compressJS = (content) => Terser.minify(content);

View File

@ -27,6 +27,8 @@
* limitations under the License.
*/
import exp from "constants";
const absolutePaths = require('./AbsolutePaths');
const deepEqual = require('fast-deep-equal/es6');
import fs from 'fs';
@ -35,6 +37,7 @@ import path from 'path';
const argv = require('./Cli').argv;
import jsonminify from 'jsonminify';
import log4js from 'log4js';
import {LogLevel} from "../models/LogLevel";
const randomString = require('./randomstring');
const suppressDisableMsg = ' -- To suppress these warning messages change ' +
'suppressErrorsInPadText to true in your settings.json\n';
@ -70,7 +73,7 @@ initLogging(defaultLogLevel, defaultLogConfig());
/* Root path of the installation */
export const root = absolutePaths.findEtherpadRoot();
logger.info('All relative paths will be interpreted relative to the identified ' +
`Etherpad base dir: ${exports.root}`);
`Etherpad base dir: ${root}`);
export const settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json');
export const credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json');
@ -93,14 +96,14 @@ export const favicon = null;
* Initialized to null, so we can spot an old configuration file and invite the
* user to update it before falling back to the default.
*/
export const skinName = null;
export let skinName = null;
export const skinVariants = 'super-light-toolbar super-light-editor light-background';
/**
* The IP ep-lite should listen to
*/
export const ip = '0.0.0.0';
export const ip:String = '0.0.0.0';
/**
* The Port ep-lite should listen to
@ -118,6 +121,12 @@ export const suppressErrorsInPadText = false;
*/
export const ssl = false;
export const sslKeys = {
cert: undefined,
key: undefined,
ca: undefined,
}
/**
* socket.io transport methods
**/
@ -142,12 +151,12 @@ export const dbType = 'dirty';
/**
* This setting is passed with dbType to ueberDB to set up the database
*/
export const dbSettings = {filename: path.join(exports.root, 'var/dirty.db')};
export const dbSettings = {filename: path.join(root, 'var/dirty.db')};
/**
* The default Text of a new pad
*/
export const defaultPadText = [
export let defaultPadText = [
'Welcome to Etherpad!',
'',
'This pad text is synchronized as you type, so that everyone viewing this page sees the same ' +
@ -244,12 +253,12 @@ export const minify = true;
/**
* The path of the abiword executable
*/
export const abiword = null;
export let abiword = null;
/**
* The path of the libreoffice executable
*/
export const soffice = null;
export let soffice = null;
/**
* The path of the tidy executable
@ -264,7 +273,7 @@ export const allowUnknownFileEnds = true;
/**
* The log level of log4js
*/
export const loglevel = defaultLogLevel;
export const loglevel:LogLevel = defaultLogLevel;
/**
* Disable IP logging
@ -299,7 +308,7 @@ export const logconfig = defaultLogConfig();
/*
* Session Key, do not sure this.
*/
export const sessionKey = false;
export let sessionKey: string|boolean = false;
/*
* Trust Proxy, whether or not trust the x-forwarded-for header.
@ -333,7 +342,7 @@ export const cookie = {
*/
export const requireAuthentication = false;
export const requireAuthorization = false;
export const users = {};
export let users = {};
/*
* Show settings in admin page, by default it is true
@ -384,6 +393,10 @@ export const exposeVersion = false;
*/
export const customLocaleStrings = {};
export const setUsers = (newUsers:any) => {
users = newUsers;
}
/*
* From Etherpad 1.8.3 onwards, import and export of pads is always rate
* limited.
@ -434,7 +447,7 @@ export const enableAdminUITests = false;
// checks if abiword is avaiable
export const abiwordAvailable = () => {
if (exports.abiword != null) {
if (abiword != null) {
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
} else {
return 'no';
@ -442,7 +455,7 @@ export const abiwordAvailable = () => {
};
export const sofficeAvailable = () => {
if (exports.soffice != null) {
if (soffice != null) {
return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes';
} else {
return 'no';
@ -450,7 +463,7 @@ export const sofficeAvailable = () => {
};
export const exportAvailable = () => {
const abiword = exports.abiwordAvailable();
const abiword = abiwordAvailable();
const soffice = sofficeAvailable();
if (abiword === 'no' && soffice === 'no') {
@ -467,7 +480,7 @@ export const exportAvailable = () => {
export const getGitCommit = () => {
let version = '';
try {
let rootPath = exports.root;
let rootPath = root;
if (fs.lstatSync(`${rootPath}/.git`).isFile()) {
rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8');
rootPath = rootPath.split(' ').pop().trim();
@ -735,88 +748,90 @@ export const reloadSettings = () => {
storeSettings(settings);
storeSettings(credentials);
initLogging(exports.loglevel, exports.logconfig);
initLogging(loglevel, logconfig);
if (!exports.skinName) {
if (!skinName) {
logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' +
'update your settings.json. Falling back to the default "colibris".');
exports.skinName = 'colibris';
skinName = 'colibris';
}
// checks if skinName has an acceptable value, otherwise falls back to "colibris"
if (exports.skinName) {
const skinBasePath = path.join(exports.root, 'src', 'static', 'skins');
const countPieces = exports.skinName.split(path.sep).length;
if (skinName) {
const skinBasePath = path.join(root, 'src', 'static', 'skins');
const countPieces = skinName.split(path.sep).length;
if (countPieces !== 1) {
logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` +
`not valid: "${exports.skinName}". Falling back to the default "colibris".`);
`not valid: "${skinName}". Falling back to the default "colibris".`);
exports.skinName = 'colibris';
skinName = 'colibris';
}
// informative variable, just for the log messages
let skinPath = path.join(skinBasePath, exports.skinName);
let skinPath = path.join(skinBasePath, skinName);
// what if someone sets skinName == ".." or "."? We catch him!
if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) {
logger.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` +
'Falling back to the default "colibris".');
exports.skinName = 'colibris';
skinPath = path.join(skinBasePath, exports.skinName);
skinName = 'colibris';
skinPath = path.join(skinBasePath, skinName);
}
if (fs.existsSync(skinPath) === false) {
logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`);
exports.skinName = 'colibris';
skinPath = path.join(skinBasePath, exports.skinName);
skinName = 'colibris';
skinPath = path.join(skinBasePath, skinName);
}
logger.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`);
logger.info(`Using skin "${skinName}" in dir: ${skinPath}`);
}
if (exports.abiword) {
if (abiword) {
// Check abiword actually exists
if (exports.abiword != null) {
fs.exists(exports.abiword, (exists: boolean) => {
if (abiword != null) {
fs.exists(abiword, (exists: boolean) => {
if (!exists) {
const abiwordError = 'Abiword does not exist at this path, check your settings file.';
if (!exports.suppressErrorsInPadText) {
exports.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`;
if (!suppressErrorsInPadText) {
defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`;
}
logger.error(`${abiwordError} File location: ${exports.abiword}`);
exports.abiword = null;
logger.error(`${abiwordError} File location: ${abiword}`);
abiword = null;
}
});
}
}
if (exports.soffice) {
fs.exists(exports.soffice, (exists: boolean) => {
if (soffice) {
fs.exists(soffice, (exists: boolean) => {
if (!exists) {
const sofficeError =
'soffice (libreoffice) does not exist at this path, check your settings file.';
if (!exports.suppressErrorsInPadText) {
exports.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`;
if (!suppressErrorsInPadText) {
defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`;
}
logger.error(`${sofficeError} File location: ${exports.soffice}`);
exports.soffice = null;
logger.error(`${sofficeError} File location: ${soffice}`);
soffice = null;
}
});
}
if (!exports.sessionKey) {
if (!sessionKey) {
const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt');
try {
exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8');
sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8');
logger.info(`Session key loaded from: ${sessionkeyFilename}`);
} catch (e) {
logger.info(
`Session key file "${sessionkeyFilename}" not found. Creating with random contents.`);
exports.sessionKey = randomString(32);
fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8');
sessionKey = randomString(32);
// FIXME Check out why this can be string boolean or Array
// @ts-ignore
fs.writeFileSync(sessionkeyFilename, sessionKey, 'utf8');
}
} else {
logger.warn('Declaring the sessionKey in the settings.json is deprecated. ' +
@ -825,17 +840,17 @@ export const reloadSettings = () => {
'Interface then you can ignore this message.');
}
if (exports.dbType === 'dirty') {
if (dbType === 'dirty') {
const dirtyWarning = 'DirtyDB is used. This is not recommended for production.';
if (!exports.suppressErrorsInPadText) {
exports.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`;
if (!suppressErrorsInPadText) {
defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`;
}
exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename);
logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`);
dbSettings.filename = absolutePaths.makeAbsolute(dbSettings.filename);
logger.warn(`${dirtyWarning} File location: ${dbSettings.filename}`);
}
if (exports.ip === '') {
if (ip === '') {
// using Unix socket for connectivity
logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' +
'"port" parameter will be interpreted as the path to a Unix socket to bind at.');
@ -852,7 +867,7 @@ export const reloadSettings = () => {
* ACHTUNG: this may prevent caching HTTP proxies to work
* TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead
*/
logger.info(`Random string used for versioning assets: ${exports.randomVersionString}`);
logger.info(`Random string used for versioning assets: ${randomVersionString}`);
};
exports.exportedForTestingOnly = {
@ -860,6 +875,6 @@ exports.exportedForTestingOnly = {
};
// initially load settings
exports.reloadSettings();
reloadSettings();

View File

@ -4,7 +4,9 @@
* Wrapper around any iterable that adds convenience methods that standard JavaScript iterable
* objects lack.
*/
class Stream {
export class Stream {
private readonly _iter: any;
private _next: null;
/**
* @returns {Stream} A Stream that yields values in the half-open range [start, end).
*/
@ -130,5 +132,3 @@ class Stream {
*/
[Symbol.iterator]() { return this._iter; }
}
module.exports = Stream;

View File

@ -3,16 +3,17 @@
* Tidy up the HTML in a given file
*/
const log4js = require('log4js');
const settings = require('./Settings');
const spawn = require('child_process').spawn;
import log4js from 'log4js';
import {tidyHtml} from "./Settings";
import {spawn} from "child_process";
exports.tidy = (srcFile) => {
const logger = log4js.getLogger('TidyHtml');
return new Promise((resolve, reject) => {
// Don't do anything if Tidy hasn't been enabled
if (!settings.tidyHtml) {
if (!tidyHtml) {
logger.debug('tidyHtml has not been configured yet, ignoring tidy request');
return resolve(null);
}
@ -21,7 +22,7 @@ exports.tidy = (srcFile) => {
// Spawn a new tidy instance that cleans up the file inline
logger.debug(`Tidying ${srcFile}`);
const tidy = spawn(settings.tidyHtml, ['-modify', srcFile]);
const tidy = spawn(tidyHtml, ['-modify', srcFile]);
// Keep track of any error messages
tidy.stderr.on('data', (data) => {

View File

@ -16,15 +16,20 @@
* limitations under the License.
*/
const Buffer = require('buffer').Buffer;
const fs = require('fs');
const fsp = fs.promises;
const path = require('path');
const zlib = require('zlib');
const settings = require('./Settings');
const existsSync = require('./path_exists');
const util = require('util');
import {Buffer} from "buffer";
import fs, {Stats} from "fs";
import path from "path";
import {check} from './path_exists'
import zlib from "zlib";
import {root} from "./Settings";
import util from "util";
const fsp = fs.promises;
/*
* The crypto module can be absent on reduced node installations.
*
@ -45,8 +50,8 @@ try {
_crypto = undefined;
}
let CACHE_DIR = path.join(settings.root, 'var/');
CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
let CACHE_DIR = path.join(root, 'var/');
CACHE_DIR = check(CACHE_DIR) ? CACHE_DIR : undefined;
const responseCache = {};
@ -78,7 +83,7 @@ if (_crypto) {
should replace this.
*/
module.exports = class CachingMiddleware {
export class CachingMiddleware {
handle(req, res, next) {
this._handle(req, res, next).catch((err) => next(err || new Error(err)));
}
@ -88,8 +93,15 @@ module.exports = class CachingMiddleware {
return next(undefined, req, res);
}
const oldReq = {};
const oldRes = {};
const oldReq = {
method: undefined
};
const oldRes = {
write: undefined,
end: undefined,
setHeader: undefined,
writeHead: undefined
};
const supportsGzip =
(req.get('Accept-Encoding') || '').indexOf('gzip') !== -1;
@ -100,7 +112,7 @@ module.exports = class CachingMiddleware {
const stats = await fsp.stat(`${CACHE_DIR}minified_${cacheKey}`).catch(() => {});
const modifiedSince =
req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']);
if (stats != null && stats.mtime && responseCache[cacheKey]) {
if (stats != null && stats instanceof Object && "mtime" in stats && responseCache[cacheKey]) {
req.headers['if-modified-since'] = stats.mtime.toUTCString();
} else {
delete req.headers['if-modified-since'];
@ -200,4 +212,4 @@ module.exports = class CachingMiddleware {
next(undefined, req, res);
}
};
}

View File

@ -7,7 +7,9 @@
* @class CustomError
* @extends {Error}
*/
class CustomError extends Error {
export class CustomError extends Error {
code: any;
signal: any;
/**
* Creates an instance of CustomError.
* @param {*} message
@ -20,5 +22,3 @@ class CustomError extends Error {
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = CustomError;

View File

@ -1,11 +1,12 @@
'use strict';
const AttributeMap = require('../../static/js/AttributeMap');
const Changeset = require('../../static/js/Changeset');
const attributes = require('../../static/js/attributes');
const exportHtml = require('./ExportHtml');
import AttributeMap from '../../static/js/AttributeMap';
import Changeset from '../../static/js/Changeset';
import attributes from '../../static/js/attributes';
import {getHTMLFromAtext} from './ExportHtml';
import {PadDiffModel} from "ep_etherpad-lite/node/models/PadDiffModel";
function PadDiff(pad, fromRev, toRev) {
export const PadDiff = (pad, fromRev, toRev)=> {
// check parameters
if (!pad || !pad.id || !pad.atext || !pad.pool) {
throw new Error('Invalid pad');
@ -14,10 +15,16 @@ function PadDiff(pad, fromRev, toRev) {
const range = pad.getValidRevisionRange(fromRev, toRev);
if (!range) throw new Error(`Invalid revision range. startRev: ${fromRev} endRev: ${toRev}`);
// FIXME How to fix this?
// @ts-ignore
this._pad = pad;
// @ts-ignore
this._fromRev = range.startRev;
// @ts-ignore
this._toRev = range.endRev;
// @ts-ignore
this._html = null;
// @ts-ignore
this._authors = [];
}
@ -108,9 +115,11 @@ PadDiff.prototype._getChangesetsInBulk = async function (startRev, count) {
return {changesets, authors};
};
PadDiff.prototype._addAuthors = function (authors) {
const self = this;
PadDiff.prototype._addAuthors = (authors)=> {
let self: undefined|PadDiffModel = this;
if(!self){
self = {_authors: []}
}
// add to array if not in the array
authors.forEach((author) => {
if (self._authors.indexOf(author) === -1) {
@ -187,7 +196,7 @@ PadDiff.prototype.getHtml = async function () {
const authorColors = await this._pad.getAllAuthorColors();
// convert the atext to html
this._html = await exportHtml.getHTMLFromAtext(this._pad, atext, authorColors);
this._html = await getHTMLFromAtext(this._pad, atext, authorColors);
return this._html;
};
@ -198,6 +207,10 @@ PadDiff.prototype.getAuthors = async function () {
if (this._html == null) {
await this.getHtml();
}
let self: undefined|PadDiffModel = this;
if(!self){
self = {_authors: []}
}
return self._authors;
};

View File

@ -1,7 +1,7 @@
'use strict';
const fs = require('fs');
import fs from 'fs';
const check = (path) => {
export const check = (path) => {
const existsSync = fs.statSync || fs.existsSync || path.existsSync;
let result;
@ -11,6 +11,4 @@ const check = (path) => {
result = false;
}
return result;
};
module.exports = check;
}

View File

@ -7,7 +7,7 @@
// `predicate`. Resolves to `undefined` if none of the Promises satisfy `predicate`, or if
// `promises` is empty. If `predicate` is nullish, the truthiness of the resolved value is used as
// the predicate.
exports.firstSatisfies = (promises, predicate) => {
export const firstSatisfies = (promises, predicate) => {
if (predicate == null) predicate = (x) => x;
// Transform each original Promise into a Promise that never resolves if the original resolved
@ -42,7 +42,7 @@ exports.firstSatisfies = (promises, predicate) => {
// `total` is greater than `concurrency`, then `concurrency` Promises will be created right away,
// and each remaining Promise will be created once one of the earlier Promises resolves.) This async
// function resolves once all `total` Promises have resolved.
exports.timesLimit = async (total, concurrency, promiseCreator) => {
export const timesLimit = async (total, concurrency, promiseCreator) => {
if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive');
let next = 0;
const addAnother = () => promiseCreator(next++).finally(() => {
@ -55,11 +55,14 @@ exports.timesLimit = async (total, concurrency, promiseCreator) => {
await Promise.all(promises);
};
/**
* An ordinary Promise except the `resolve` and `reject` executor functions are exposed as
* properties.
*/
class Gate extends Promise {
//FIXME Why is the constructor diviating from the Promise constructor?
// @ts-ignore
export class Gate extends Promise {
// Coax `.then()` into returning an ordinary Promise, not a Gate. See
// https://stackoverflow.com/a/65669070 for the rationale.
static get [Symbol.species]() { return Promise; }
@ -73,4 +76,3 @@ class Gate extends Promise {
Object.assign(this, props);
}
}
exports.Gate = Gate;

View File

@ -1,12 +1,12 @@
'use strict';
const spawn = require('cross-spawn');
const log4js = require('log4js');
const path = require('path');
const settings = require('./Settings');
import spawn from 'cross-spawn';
import log4js from 'log4js';
import path from 'path';
import {root} from "./Settings";
import {CMDOptions, CMDPromise} from '../models/CMDOptions'
const logger = log4js.getLogger('runCmd');
import {CustomError} from './customError'
const logLines = (readable, logLineFn) => {
readable.setEncoding('utf8');
// The process won't necessarily write full lines every time -- it might write a part of a line
@ -69,33 +69,40 @@ const logLines = (readable, logLineFn) => {
* - `stderr`: Similar to `stdout` but for stderr.
* - `child`: The ChildProcess object.
*/
module.exports = exports = (args, opts = {}) => {
export const exportCMD: (args: string[], opts:CMDOptions)=>void = async (args, opts = {
cwd: undefined,
stdio: undefined,
env: undefined
}) => {
logger.debug(`Executing command: ${args.join(' ')}`);
opts = {cwd: settings.root, ...opts};
opts = {cwd: root, ...opts};
logger.debug(`cwd: ${opts.cwd}`);
// Log stdout and stderr by default.
const stdio =
Array.isArray(opts.stdio) ? opts.stdio.slice() // Copy to avoid mutating the caller's array.
: typeof opts.stdio === 'function' ? [null, opts.stdio, opts.stdio]
: opts.stdio === 'string' ? [null, 'string', 'string']
: Array(3).fill(opts.stdio);
: typeof opts.stdio === 'function' ? [null, opts.stdio, opts.stdio]
: opts.stdio === 'string' ? [null, 'string', 'string']
: Array(3).fill(opts.stdio);
const cmdLogger = log4js.getLogger(`runCmd|${args[0]}`);
if (stdio[1] == null) stdio[1] = (line) => cmdLogger.info(line);
if (stdio[2] == null) stdio[2] = (line) => cmdLogger.error(line);
if (stdio[1] == null && stdio instanceof Array) stdio[1] = (line) => cmdLogger.info(line);
if (stdio[2] == null && stdio instanceof Array) stdio[2] = (line) => cmdLogger.error(line);
const stdioLoggers = [];
const stdioSaveString = [];
for (const fd of [1, 2]) {
if (typeof stdio[fd] === 'function') {
stdioLoggers[fd] = stdio[fd];
stdio[fd] = 'pipe';
if (stdio instanceof Array)
stdio[fd] = 'pipe';
} else if (stdio[fd] === 'string') {
stdioSaveString[fd] = true;
stdio[fd] = 'pipe';
if (stdio instanceof Array)
stdio[fd] = 'pipe';
}
}
opts.stdio = stdio;
if (opts.stdio instanceof Array) {
opts.stdio = stdio;
}
// On Windows the PATH environment var might be spelled "Path".
const pathVarName =
@ -107,8 +114,8 @@ module.exports = exports = (args, opts = {}) => {
opts.env = {
...env, // Copy env to avoid modifying process.env or the caller's supplied env.
[pathVarName]: [
path.join(settings.root, 'src', 'node_modules', '.bin'),
path.join(settings.root, 'node_modules', '.bin'),
path.join(root, 'src', 'node_modules', '.bin'),
path.join(root, 'node_modules', '.bin'),
...(PATH ? PATH.split(path.delimiter) : []),
].join(path.delimiter),
};
@ -116,13 +123,15 @@ module.exports = exports = (args, opts = {}) => {
// Create an error object to use in case the process fails. This is done here rather than in the
// process's `exit` handler so that we get a useful stack trace.
const procFailedErr = new Error();
const procFailedErr:CustomError = new CustomError({});
const proc = spawn(args[0], args.slice(1), opts);
const streams = [undefined, proc.stdout, proc.stderr];
let px;
const p = new Promise((resolve, reject) => { px = {resolve, reject}; });
const p = await new Promise<CMDPromise>((resolve, reject) => {
px = {resolve, reject};
});
[, p.stdout, p.stderr] = streams;
p.child = proc;
@ -132,6 +141,8 @@ module.exports = exports = (args, opts = {}) => {
if (stdioLoggers[fd] != null) {
logLines(streams[fd], stdioLoggers[fd]);
} else if (stdioSaveString[fd]) {
//FIXME How to solve this?
// @ts-ignore
p[[null, 'stdout', 'stderr'][fd]] = stdioStringPromises[fd] = (async () => {
const chunks = [];
for await (const chunk of streams[fd]) chunks.push(chunk);

View File

@ -1,10 +1,10 @@
'use strict';
const path = require('path');
import path from 'path';
// Normalizes p and ensures that it is a relative path that does not reach outside. See
// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context.
module.exports = (p, pathApi = path) => {
export default (p, pathApi = path) => {
// The documentation for path.normalize() says that it resolves '..' and '.' segments. The word
// "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might
// not be the same thing as 'b'. Most path normalization functions from other libraries (e.g.,
@ -20,4 +20,4 @@ module.exports = (p, pathApi = path) => {
// pathname would not be normalized away before being converted to '../'.
if (pathApi.sep === '\\') p = p.replace(/\\/g, '/');
return p;
};
}

View File

@ -2,7 +2,7 @@
/**
* The Toolbar Module creates and renders the toolbars and buttons
*/
const _ = require('underscore');
import _ from 'underscore';
const removeItem = (array, what) => {
let ax;
@ -12,13 +12,13 @@ const removeItem = (array, what) => {
return array;
};
const defaultButtonAttributes = (name, overrides) => ({
const defaultButtonAttributes = (name, overrides?) => ({
command: name,
localizationId: `pad.toolbar.${name}.title`,
class: `buttonicon buttonicon-${name}`,
});
const tag = (name, attributes, contents) => {
const tag = (name, attributes, contents?) => {
const aStr = tagAttributes(attributes);
if (_.isString(contents) && contents.length > 0) {

23
src/package-lock.json generated
View File

@ -58,6 +58,8 @@
},
"devDependencies": {
"@types/express": "4.17.17",
"@types/jquery": "^3.5.16",
"@types/js-cookie": "^3.0.3",
"@types/node": "^20.3.1",
"concurrently": "^8.2.0",
"eslint": "^8.14.0",
@ -1081,6 +1083,21 @@
"@types/unist": "*"
}
},
"node_modules/@types/jquery": {
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz",
"integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==",
"dev": true,
"dependencies": {
"@types/sizzle": "*"
}
},
"node_modules/@types/js-cookie": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz",
"integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
@ -1181,6 +1198,12 @@
"@types/node": "*"
}
},
"node_modules/@types/sizzle": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
"dev": true
},
"node_modules/@types/stoppable": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.1.tgz",

View File

@ -78,23 +78,25 @@
"etherpad-lite": "node/server.js"
},
"devDependencies": {
"@types/express": "4.17.17",
"@types/jquery": "^3.5.16",
"@types/js-cookie": "^3.0.3",
"@types/node": "^20.3.1",
"concurrently": "^8.2.0",
"eslint": "^8.14.0",
"eslint-config-etherpad": "^3.0.13",
"etherpad-cli-client": "^2.0.1",
"mocha": "^9.2.2",
"mocha-froth": "^0.2.10",
"nodeify": "^1.0.1",
"nodemon": "^2.0.22",
"openapi-schema-validation": "^0.4.2",
"selenium-webdriver": "^4.10.0",
"set-cookie-parser": "^2.4.8",
"sinon": "^13.0.2",
"split-grid": "^1.0.11",
"supertest": "^6.3.3",
"typescript": "^4.9.5",
"@types/node": "^20.3.1",
"@types/express": "4.17.17",
"concurrently": "^8.2.0",
"nodemon": "^2.0.22"
"typescript": "^4.9.5"
},
"engines": {
"node": ">=14.15.0",

View File

@ -22,11 +22,11 @@
* limitations under the License.
*/
const isNodeText = (node) => (node.nodeType === 3);
export const isNodeText = (node) => (node.nodeType === 3);
const getAssoc = (obj, name) => obj[`_magicdom_${name}`];
export const getAssoc = (obj, name) => obj[`_magicdom_${name}`];
const setAssoc = (obj, name, value) => {
export const setAssoc = (obj, name, value) => {
// note that in IE designMode, properties of a node can get
// copied to new nodes that are spawned during editing; also,
// properties representable in HTML text can survive copy-and-paste
@ -38,7 +38,7 @@ const setAssoc = (obj, name, value) => {
// between false and true, a number between 0 and numItems inclusive.
const binarySearch = (numItems, func) => {
export const binarySearch = (numItems, func) => {
if (numItems < 1) return 0;
if (func(0)) return 0;
if (!func(numItems - 1)) return numItems;
@ -52,17 +52,10 @@ const binarySearch = (numItems, func) => {
return high;
};
const binarySearchInfinite = (expectedLength, func) => {
export const binarySearchInfinite = (expectedLength, func) => {
let i = 0;
while (!func(i)) i += expectedLength;
return binarySearch(i, func);
};
const noop = () => {};
exports.isNodeText = isNodeText;
exports.getAssoc = getAssoc;
exports.setAssoc = setAssoc;
exports.binarySearch = binarySearch;
exports.binarySearchInfinite = binarySearchInfinite;
exports.noop = noop;
export const noop = () => {};

View File

@ -22,13 +22,13 @@
* limitations under the License.
*/
const Security = require('./security');
import Security from './security';
/**
* Generates a random String with the given length. Is needed to generate the Author, Group,
* readonly, session Ids
*/
const randomString = (len) => {
const randomString = (len?) => {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
let randomstring = '';
len = len || 20;
@ -91,7 +91,9 @@ const urlRegex = (() => {
// https://stackoverflow.com/a/68957976
const base64url = /^(?=(?:.{4})*$)[A-Za-z0-9_-]*(?:[AQgw]==|[AEIMQUYcgkosw048]=)?$/;
const padutils = {
export const padutils = {
setupGlobalExceptionHandler: undefined,
/**
* Prints a warning message followed by a stack trace (to make it easier to figure out what code
* is using the deprecated function).
@ -107,25 +109,32 @@ const padutils = {
* @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no
* logger is set), with a stack trace appended if available.
*/
warnDeprecated: (...args) => {
if (padutils.warnDeprecated.disabledForTestingOnly) return;
warnDeprecated: (...args) => {
if ("disabledForTestingOnly" in padutils.warnDeprecated) return;
const err = new Error();
if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnDeprecated);
err.name = '';
// Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam.
if (typeof err.stack === 'string') {
if (padutils.warnDeprecated._rl == null) {
if (typeof err.stack === 'string' && "_rl" in padutils.warnDeprecated) {
padutils.warnDeprecated._rl =
{prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000};
}
const rl = padutils.warnDeprecated._rl;
const now = rl.now();
const prev = rl.prevs.get(err.stack);
if (prev != null && now - prev < rl.period) return;
rl.prevs.set(err.stack, now);
if (rl instanceof Object && "now" in rl && "prevs" in rl && "period" in rl
&& rl.now instanceof Function && rl.prevs instanceof Map && rl.period instanceof Number){
const now = rl.now();
const prev = rl.prevs.get(err.stack);
if (prev != null && now - prev < rl.period) return;
rl.prevs.set(err.stack, now);
}
}
if (err.stack) args.push(err.stack);
(padutils.warnDeprecated.logger || console).warn(...args);
if ("logger" in padutils.warnDeprecated && padutils.warnDeprecated.logger instanceof Object
&& "warn" in padutils.warnDeprecated.logger && padutils.warnDeprecated.logger.warn instanceof Function){
padutils.warnDeprecated.logger.warn(...args)
}
else{
console.warn(...args)
}
},
escapeHtml: (x) => Security.escapeHTML(String(x)),
@ -350,7 +359,11 @@ const padutils = {
* Returns a string that can be used in the `token` cookie as a secret that authenticates a
* particular author.
*/
generateAuthorToken: () => `t.${randomString()}`,
generateAuthorToken: () => {
const randomAuthToken = randomString()
return `t.+${randomAuthToken}`
}
};
let globalExceptionHandler = null;
@ -400,6 +413,8 @@ padutils.setupGlobalExceptionHandler = () => {
.append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')),
];
//FIXME gritter not defined
// @ts-ignore
$.gritter.add({
title: 'An error occurred',
text: errorMsg,
@ -429,7 +444,7 @@ padutils.setupGlobalExceptionHandler = () => {
}
};
padutils.binarySearch = require('./ace2_common').binarySearch;
import {binarySearch} from '../../static/js/ace2_common';
// https://stackoverflow.com/a/42660748
const inThirdPartyIframe = () => {
@ -439,11 +454,12 @@ const inThirdPartyIframe = () => {
return true;
}
};
import cookies from 'js-cookie/dist/js.cookie';
// This file is included from Node so that it can reuse randomString, but Node doesn't have a global
// window object.
if (typeof window !== 'undefined') {
exports.Cookies = require('js-cookie/dist/js.cookie').withAttributes({
cookies.withAttributes({
// Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case
// use `SameSite=None`. For iframes from another site, only `None` has a chance of working
// because the cookies are third-party (not same-site). Many browsers/users block third-party
@ -456,5 +472,3 @@ if (typeof window !== 'undefined') {
secure: window.location.protocol === 'https:',
});
}
exports.randomString = randomString;
exports.padutils = padutils;

View File

@ -1,5 +1,7 @@
{
"compilerOptions": {
"typeRoots": ["node_modules/@types"],
"types": ["node", "jquery"],
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",