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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@
import {db} from './DB'; 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 {editOnly, loadTest, requireAuthentication, requireSession} from "../utils/Settings";
import webaccess from "../hooks/express/webaccess"; import {normalizeAuthzLevel} from "../hooks/express/webaccess";
import log4js from "log4js"; 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 // Note: userSettings.padAuthorizations should still be populated even if
// settings.requireAuthorization is false. // settings.requireAuthorization is false.
const padAuthzs = userSettings.padAuthorizations || {}; const padAuthzs = userSettings.padAuthorizations || {};
const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]); const level = normalizeAuthzLevel(padAuthzs[padID]);
if (!level) { if (!level) {
authLogger.debug('access denied: unauthorized'); authLogger.debug('access denied: unauthorized');
return DENY; return DENY;

View File

@ -19,12 +19,12 @@
* limitations under the License. * limitations under the License.
*/ */
import absolutePaths from '../utils/AbsolutePaths'; import {makeAbsolute} from '../utils/AbsolutePaths';
import fs from 'fs'; import fs from 'fs';
import * as api from '../db/API'; import * as api from '../db/API';
import log4js from 'log4js'; import log4js from 'log4js';
import {sanitizePadId} from '../db/PadManager'; import {sanitizePadId} from '../db/PadManager';
import randomString from '../utils/randomstring'; import {randomString} from '../utils/randomstring';
const argv = require('../utils/Cli').argv; const argv = require('../utils/Cli').argv;
const createHTTPError = require('http-errors'); const createHTTPError = require('http-errors');
@ -32,7 +32,7 @@ const apiHandlerLogger = log4js.getLogger('APIHandler');
// ensure we have an apikey // ensure we have an apikey
let apikey = null; let apikey = null;
const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt'); const apikeyFilename = makeAbsolute(argv.apikey || './APIKEY.txt');
try { try {
apikey = fs.readFileSync(apikeyFilename, 'utf8'); 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 {abiword, allowUnknownFileEnds, importMaxFileSize, soffice} from '../utils/Settings';
import {Formidable} from 'formidable'; import {Formidable} from 'formidable';
import os from 'os'; import os from 'os';
import importHtml from '../utils/ImportHtml'; import {setPadHTML} from '../utils/ImportHtml';
import importEtherpad from '../utils/ImportEtherpad'; import {setPadRaw} from '../utils/ImportEtherpad';
import log4js from 'log4js'; import log4js from 'log4js';
import hooks from '../../static/js/pluginfw/hooks.js'; 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'); const text = await fs.readFile(srcFile, 'utf8');
directDatabaseAccess = true; directDatabaseAccess = true;
await importEtherpad.setPadRaw(padId, text, authorId); await setPadRaw(padId, text, authorId);
} }
// convert file to html if necessary // convert file to html if necessary
@ -207,7 +207,7 @@ const doImport = async (req, res, padId, authorId) => {
if (!directDatabaseAccess) { if (!directDatabaseAccess) {
if (importHandledByPlugin || useConverter || fileIsHTML) { if (importHandledByPlugin || useConverter || fileIsHTML) {
try { try {
await importHtml.setPadHTML(pad, text, authorId); await setPadHTML(pad, text, authorId);
} catch (err) { } catch (err) {
logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || 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 {strict as assert} from "assert";
import {RateLimiterMemory} from 'rate-limiter-flexible'; import {RateLimiterMemory} from 'rate-limiter-flexible';
import webaccess from '../hooks/express/webaccess'; import {userCanModify} from '../hooks/express/webaccess';
import {ErrorCaused} from "../models/ErrorCaused"; import {ErrorCaused} from "../models/ErrorCaused";
import {Pad} from "../db/Pad"; import {Pad} from "../db/Pad";
import {SessionInfo} from "../models/SessionInfo"; 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 * This Method is called by server.js to tell the message handler on which socket it should send
* @param socket_io The Socket * @param socket_io The Socket
*/ */
exports.setSocketIO = (socket_io) => { export const setSocketIO = (socket_io) => {
socketio = socket_io; socketio = socket_io;
}; };
@ -172,7 +172,7 @@ exports.setSocketIO = (socket_io) => {
* Handles the connection of a new user * Handles the connection of a new user
* @param socket the socket.io Socket object for the new connection from the client * @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(); createCollection.meter('connects').mark();
// Initialize sessioninfos for this new session // Initialize sessioninfos for this new session
@ -186,7 +186,7 @@ exports.handleConnect = (socket) => {
/** /**
* Kicks all sessions from a pad * Kicks all sessions from a pad
*/ */
exports.kickSessionsFromPad = (padID) => { export const kickSessionsFromPad = (padID) => {
if (typeof socketio.sockets.clients !== 'function') return; if (typeof socketio.sockets.clients !== 'function') return;
// skip if there is nobody on this pad // skip if there is nobody on this pad
@ -200,7 +200,7 @@ exports.kickSessionsFromPad = (padID) => {
* Handles the disconnection of a user * Handles the disconnection of a user
* @param socket the socket.io Socket object for the client * @param socket the socket.io Socket object for the client
*/ */
exports.handleDisconnect = async (socket) => { export const handleDisconnect = async (socket) => {
createCollection.meter('disconnects').mark(); createCollection.meter('disconnects').mark();
const session = sessioninfos[socket.id]; const session = sessioninfos[socket.id];
delete 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 socket the socket.io Socket object for the client
* @param message the message from 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'; const env = process.env.NODE_ENV || 'development';
if (env === 'production') { if (env === 'production') {
@ -272,7 +272,7 @@ exports.handleMessage = async (socket, message) => {
thisSession.padId = padIds.padId; thisSession.padId = padIds.padId;
thisSession.readOnlyPadId = padIds.readOnlyPadId; thisSession.readOnlyPadId = padIds.readOnlyPadId;
thisSession.readonly = 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 // 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 // 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 msg {Object} the message we're sending
* @param sessionID {string} the socketIO session to which we're sending this message * @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 (msg.data.type === 'CUSTOM') {
if (sessionID) { if (sessionID) {
// a sessionID is targeted: directly to this 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}}); socket.json.send({type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev}});
thisSession.rev = newRev; thisSession.rev = newRev;
if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev); if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev);
await exports.updatePadClients(pad); await updatePadClients(pad);
} catch (err) { } catch (err) {
socket.json.send({disconnect: 'badChangeset'}); socket.json.send({disconnect: 'badChangeset'});
createCollection.meter('failedChangesets').mark(); createCollection.meter('failedChangesets').mark();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,14 @@
'use strict'; 'use strict';
const path = require('path'); import path from 'path';
const eejs = require('../../eejs'); import {required} from '../../eejs';
const fs = require('fs'); import fs from 'fs';
const fsp = fs.promises; const fsp = fs.promises;
const toolbar = require('../../utils/toolbar'); import {} from '../../utils/toolbar';
const hooks = require('../../../static/js/pluginfw/hooks'); import hooks from '../../../static/js/pluginfw/hooks';
const settings = require('../../utils/Settings'); import {favicon, getEpVersion, maxAge, root, skinName} from '../../utils/Settings';
const util = require('util'); import util from 'util';
const webaccess = require('./webaccess'); import {userCanModify} from './webaccess';
exports.expressPreSession = async (hookName, {app}) => { exports.expressPreSession = async (hookName, {app}) => {
// This endpoint is intended to conform to: // This endpoint is intended to conform to:
@ -17,25 +17,25 @@ exports.expressPreSession = async (hookName, {app}) => {
res.set('Content-Type', 'application/health+json'); res.set('Content-Type', 'application/health+json');
res.json({ res.json({
status: 'pass', status: 'pass',
releaseId: settings.getEpVersion(), releaseId: getEpVersion(),
}); });
}); });
app.get('/stats', (req, res) => { app.get('/stats', (req, res) => {
res.json(require('../../stats').toJSON()); res.json(required('../../stats').toJSON());
}); });
app.get('/javascript', (req, res) => { 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) => { app.get('/robots.txt', (req, res) => {
let filePath = 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) => { res.sendFile(filePath, (err) => {
// there is no custom robots.txt, send the default robots.txt which dissallows all // there is no custom robots.txt, send the default robots.txt which dissallows all
if (err) { if (err) {
filePath = path.join(settings.root, 'src', 'static', 'robots.txt'); filePath = path.join(root, 'src', 'static', 'robots.txt');
res.sendFile(filePath); res.sendFile(filePath);
} }
}); });
@ -44,9 +44,9 @@ exports.expressPreSession = async (hookName, {app}) => {
app.get('/favicon.ico', (req, res, next) => { app.get('/favicon.ico', (req, res, next) => {
(async () => { (async () => {
const fns = [ const fns = [
...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []), ...(favicon ? [path.resolve(root, favicon)] : []),
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'), path.join(root, 'src', 'static', 'skins', skinName, 'favicon.ico'),
path.join(settings.root, 'src', 'static', 'favicon.ico'), path.join(root, 'src', 'static', 'favicon.ico'),
]; ];
for (const fn of fns) { for (const fn of fns) {
try { try {
@ -54,7 +54,7 @@ exports.expressPreSession = async (hookName, {app}) => {
} catch (err) { } catch (err) {
continue; 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); await util.promisify(res.sendFile.bind(res))(fn);
return; return;
} }
@ -66,13 +66,13 @@ exports.expressPreSession = async (hookName, {app}) => {
exports.expressCreateServer = (hookName, args, cb) => { exports.expressCreateServer = (hookName, args, cb) => {
// serve index.html under / // serve index.html under /
args.app.get('/', (req, res) => { 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 // serve pad.html under /p
args.app.get('/p/:pad', (req, res, next) => { args.app.get('/p/:pad', (req, res, next) => {
// The below might break for pads being rewritten // 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', { hooks.callAll('padInitToolbar', {
toolbar, toolbar,
@ -81,7 +81,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
// can be removed when require-kernel is dropped // can be removed when require-kernel is dropped
res.header('Feature-Policy', 'sync-xhr \'self\''); 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, req,
toolbar, toolbar,
isReadOnly, isReadOnly,
@ -94,7 +94,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
toolbar, toolbar,
}); });
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', { res.send(required('ep_etherpad-lite/templates/timeslider.html', {
req, req,
toolbar, 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 tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8');
const tar = {}; const tar = {};
for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) { for (let [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) {
const files = relativeFiles.map(prefixLocalLibraryPath); const relativeFilesMapped = relativeFiles as string[];
const files = relativeFilesMapped.map(prefixLocalLibraryPath);
tar[prefixLocalLibraryPath(key)] = files tar[prefixLocalLibraryPath(key)] = files
.concat(files.map((p) => p.replace(/\.js$/, ''))) .concat(files.map((p) => p.replace(/\.js$/, '')))
.concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`)); .concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`));
@ -28,7 +29,7 @@ const getTar = async () => {
return tar; return tar;
}; };
exports.expressPreSession = async (hookName, {app}) => { export const expressPreSession = async (hookName, {app}) => {
// Cache both minified and static. // Cache both minified and static.
const assetCache = new CachingMiddleware(); const assetCache = new CachingMiddleware();
app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache)); 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 clientParts = plugins.parts.filter((part) => part.client_hooks != null);
const clientPlugins = {}; const clientPlugins = {};
for (const name of new Set(clientParts.map((part) => part.plugin))) { for (const name of new Set(clientParts.map((part) => part.plugin))) {
clientPlugins[name] = {...plugins.plugins[name]}; const mappedName = name as any
delete clientPlugins[name].package; clientPlugins[mappedName] = {...plugins.plugins[mappedName]};
delete clientPlugins[mappedName].package;
} }
res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);

View File

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

View File

@ -1,12 +1,17 @@
'use strict'; 'use strict';
const assert = require('assert').strict; import {strict as assert} from "assert";
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 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'; hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
// Promisified wrapper around hooks.aCallFirst. // Promisified wrapper around hooks.aCallFirst.
@ -17,7 +22,7 @@ const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, rej
const aCallFirst0 = const aCallFirst0 =
async (hookName, context, pred = null) => (await aCallFirst(hookName, context, pred))[0]; async (hookName, context, pred = null) => (await aCallFirst(hookName, context, pred))[0];
exports.normalizeAuthzLevel = (level) => { export const normalizeAuthzLevel = (level) => {
if (!level) return false; if (!level) return false;
switch (level) { switch (level) {
case true: case true:
@ -32,20 +37,20 @@ exports.normalizeAuthzLevel = (level) => {
return false; return false;
}; };
exports.userCanModify = (padId, req) => { export const userCanModify = (padId, req) => {
if (readOnlyManager.isReadOnlyId(padId)) return false; if (isReadOnlyId(padId)) return false;
if (!settings.requireAuthentication) return true; if (!requireAuthentication) return true;
const {session: {user} = {}} = req; const {session: {user} = {}}:SessionSocketModel = req;
if (!user || user.readOnly) return false; if (!user || user.readOnly) return false;
assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization. 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'; return level && level !== 'readOnly';
}; };
// Exported so that tests can set this to 0 to avoid unnecessary test slowness. // 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'); 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). // authentication is checked and once after (if settings.requireAuthorization is true).
const authorize = async () => { const authorize = async () => {
const grant = async (level) => { const grant = async (level) => {
level = exports.normalizeAuthzLevel(level); level = normalizeAuthzLevel(level);
if (!level) return false; if (!level) return false;
const user = req.session.user; const user = req.session.user;
if (user == null) return true; // This will happen if authentication is not required. if (user == null) return true; // This will happen if authentication is not required.
const encodedPadId = (req.path.match(/^\/p\/([^/]*)/) || [])[1]; const encodedPadId = (req.path.match(/^\/p\/([^/]*)/) || [])[1];
if (encodedPadId == null) return true; if (encodedPadId == null) return true;
let padId = decodeURIComponent(encodedPadId); let padId = decodeURIComponent(encodedPadId);
if (readOnlyManager.isReadOnlyId(padId)) { if (isReadOnlyId(padId)) {
// pad is read-only, first get the real pad ID // pad is read-only, first get the real pad ID
padId = await readOnlyManager.getPadId(padId); padId = await getPadId(padId);
if (padId == null) return false; if (padId == null) return false;
} }
// The user was granted access to a pad. Remember the authorization level in the user's // 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; const isAuthenticated = req.session && req.session.user;
if (isAuthenticated && req.session.user.is_admin) return await grant('create'); 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 (!requireAuthn) return await grant('create');
if (!isAuthenticated) return await grant(false); if (!isAuthenticated) return await grant(false);
if (requireAdmin && !req.session.user.is_admin) 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})); return await grant(await aCallFirst0('authorize', {req, res, next, resource: req.path}));
}; };
@ -131,8 +136,10 @@ const checkAccess = async (req, res, next) => {
// page). // page).
// /////////////////////////////////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////////////////////////////////
if (settings.users == null) settings.users = {}; if (users == null) setUsers({});
const ctx = {req, res, users: settings.users, next}; 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 // If the HTTP basic auth header is present, extract the username and password so it can be given
// to authn plugins. // to authn plugins.
const httpBasicAuth = req.headers.authorization && req.headers.authorization.startsWith('Basic '); 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))) { if (!(await aCallFirst0('authenticate', ctx))) {
// Fall back to HTTP basic auth. // 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) { if (!httpBasicAuth || !ctx.username || password == null || password !== ctx.password) {
httpLogger.info(`Failed authentication from IP ${req.ip}`); httpLogger.info(`Failed authentication from IP ${req.ip}`);
if (await aCallFirst0('authnFailure', {req, res})) return; 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. // No plugin handled the authentication failure. Fall back to basic authentication.
res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
// Delay the error response for 1s to slow down brute force attacks. // 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'); res.status(401).send('Authentication Required');
return; 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 // 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. // 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; delete req.session.user.password;
} }
if (req.session.user == null) { 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 middleware to authenticate the user and check authorization. Must be installed after the
* express-session middleware. * 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))); checkAccess(req, res, next).catch((err) => next(err || new Error(err)));
}; };

View File

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

View File

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

View File

@ -19,9 +19,10 @@
* limitations under the License. * limitations under the License.
*/ */
const log4js = require('log4js'); import log4js from 'log4js';
const path = require('path'); import path from "path";
const _ = require('underscore');
import _ from "underscore";
const absPathLogger = log4js.getLogger('AbsolutePaths'); 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 * @return {string} The identified absolute base path. If such path cannot be
* identified, prints a log and exits the application. * identified, prints a log and exits the application.
*/ */
exports.findEtherpadRoot = () => { export const findEtherpadRoot = () => {
if (etherpadRoot != null) { if (etherpadRoot != null) {
return etherpadRoot; return etherpadRoot;
} }
@ -131,12 +132,12 @@ exports.findEtherpadRoot = () => {
* it is returned unchanged. Otherwise it is interpreted * it is returned unchanged. Otherwise it is interpreted
* relative to exports.root. * relative to exports.root.
*/ */
exports.makeAbsolute = (somePath) => { export const makeAbsolute = (somePath) => {
if (path.isAbsolute(somePath)) { if (path.isAbsolute(somePath)) {
return 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}"`); absPathLogger.debug(`Relative path "${somePath}" can be rewritten to "${rewrittenPath}"`);
return rewrittenPath; return rewrittenPath;
@ -150,7 +151,7 @@ exports.makeAbsolute = (somePath) => {
* a subdirectory of the base one * a subdirectory of the base one
* @return {boolean} * @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 // 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 relative = path.relative(parent, arbitraryDir);
const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);

View File

@ -21,9 +21,8 @@
*/ */
// An object containing the parsed command-line options // 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; let arg, prevArg;
// Loop through args // Loop through args

View File

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

View File

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

View File

@ -15,16 +15,19 @@
* limitations under the License. * limitations under the License.
*/ */
const Changeset = require('../../static/js/Changeset'); import Changeset from '../../static/js/Changeset';
const attributes = require('../../static/js/attributes'); import attributes from "../../static/js/attributes";
const padManager = require('../db/PadManager');
const _ = require('underscore'); import {getPad} from "../db/PadManager";
const Security = require('../../static/js/security');
const hooks = require('../../static/js/pluginfw/hooks'); import _ from "underscore";
const eejs = require('../eejs');
const _analyzeLine = require('./ExportHelper')._analyzeLine; import Security from '../../static/js/security';
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; import hooks from '../../static/js/pluginfw/hooks';
const padutils = require('../../static/js/pad_utils').padutils; import {required} from '../eejs';
import {_analyzeLine, _encodeWhitespace} from "./ExportHelper";
import {padutils} from "../../static/js/pad_utils";
const getPadHTML = async (pad, revNum) => { const getPadHTML = async (pad, revNum) => {
let atext = pad.atext; let atext = pad.atext;
@ -38,7 +41,7 @@ const getPadHTML = async (pad, revNum) => {
return await getHTMLFromAtext(pad, atext); return await getHTMLFromAtext(pad, atext);
}; };
const getHTMLFromAtext = async (pad, atext, authorColors) => { export const getHTMLFromAtext = async (pad, atext, authorColors?) => {
const apool = pad.apool(); const apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n'); const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
@ -456,8 +459,8 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
return pieces.join(''); return pieces.join('');
}; };
exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => { export const getPadHTMLDocument = async (padId, revNum, readOnlyId?) => {
const pad = await padManager.getPad(padId); const pad = await getPad(padId);
// Include some Styles into the Head for Export // Include some Styles into the Head for Export
let stylesForExportCSS = ''; let stylesForExportCSS = '';
@ -472,7 +475,7 @@ exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => {
html += hookHtml; html += hookHtml;
} }
return eejs.require('ep_etherpad-lite/templates/export_html.html', { return required('ep_etherpad-lite/templates/export_html.html', {
body: html, body: html,
padId: Security.escapeHTML(readOnlyId || padId), padId: Security.escapeHTML(readOnlyId || padId),
extraCSS: stylesForExportCSS, extraCSS: stylesForExportCSS,
@ -525,7 +528,4 @@ const _processSpaces = (s) => {
} }
} }
return parts.join(''); 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 // This is different than the functionality provided in ExportHtml as it provides formatting
// functionality that is designed specifically for TXT exports // functionality that is designed specifically for TXT exports
const getTXTFromAtext = (pad, atext, authorColors) => { export const getTXTFromAtext = (pad, atext, authorColors?) => {
const apool = pad.apool(); const apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n'); const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
@ -56,7 +56,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
}); });
const getLineTXT = (text, attribs) => { const getLineTXT = (text, attribs) => {
const propVals = [false, false, false]; const propVals:(boolean|number)[] = [false, false, false];
const ENTER = 1; const ENTER = 1;
const STAY = 2; const STAY = 2;
const LEAVE = 0; const LEAVE = 0;
@ -256,9 +256,8 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
return pieces.join(''); return pieces.join('');
}; };
exports.getTXTFromAtext = getTXTFromAtext;
exports.getPadTXTDocument = async (padId, revNum) => { export const getPadTXTDocument = async (padId, revNum) => {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
return getPadTXT(pad, revNum); return getPadTXT(pad, revNum);
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,9 @@
* Wrapper around any iterable that adds convenience methods that standard JavaScript iterable * Wrapper around any iterable that adds convenience methods that standard JavaScript iterable
* objects lack. * 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). * @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; } [Symbol.iterator]() { return this._iter; }
} }
module.exports = Stream;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
'use strict'; '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 // 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. // 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 // 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 // "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., // 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 '../'. // pathname would not be normalized away before being converted to '../'.
if (pathApi.sep === '\\') p = p.replace(/\\/g, '/'); if (pathApi.sep === '\\') p = p.replace(/\\/g, '/');
return p; return p;
}; }

View File

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

23
src/package-lock.json generated
View File

@ -58,6 +58,8 @@
}, },
"devDependencies": { "devDependencies": {
"@types/express": "4.17.17", "@types/express": "4.17.17",
"@types/jquery": "^3.5.16",
"@types/js-cookie": "^3.0.3",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"concurrently": "^8.2.0", "concurrently": "^8.2.0",
"eslint": "^8.14.0", "eslint": "^8.14.0",
@ -1081,6 +1083,21 @@
"@types/unist": "*" "@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": { "node_modules/@types/json-schema": {
"version": "7.0.9", "version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
@ -1181,6 +1198,12 @@
"@types/node": "*" "@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": { "node_modules/@types/stoppable": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.1.tgz",

View File

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

View File

@ -22,11 +22,11 @@
* limitations under the License. * 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 // note that in IE designMode, properties of a node can get
// copied to new nodes that are spawned during editing; also, // copied to new nodes that are spawned during editing; also,
// properties representable in HTML text can survive copy-and-paste // 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. // 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 (numItems < 1) return 0;
if (func(0)) return 0; if (func(0)) return 0;
if (!func(numItems - 1)) return numItems; if (!func(numItems - 1)) return numItems;
@ -52,17 +52,10 @@ const binarySearch = (numItems, func) => {
return high; return high;
}; };
const binarySearchInfinite = (expectedLength, func) => { export const binarySearchInfinite = (expectedLength, func) => {
let i = 0; let i = 0;
while (!func(i)) i += expectedLength; while (!func(i)) i += expectedLength;
return binarySearch(i, func); return binarySearch(i, func);
}; };
const noop = () => {}; export const noop = () => {};
exports.isNodeText = isNodeText;
exports.getAssoc = getAssoc;
exports.setAssoc = setAssoc;
exports.binarySearch = binarySearch;
exports.binarySearchInfinite = binarySearchInfinite;
exports.noop = noop;

View File

@ -22,13 +22,13 @@
* limitations under the License. * 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, * Generates a random String with the given length. Is needed to generate the Author, Group,
* readonly, session Ids * readonly, session Ids
*/ */
const randomString = (len) => { const randomString = (len?) => {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
let randomstring = ''; let randomstring = '';
len = len || 20; len = len || 20;
@ -91,7 +91,9 @@ const urlRegex = (() => {
// https://stackoverflow.com/a/68957976 // https://stackoverflow.com/a/68957976
const base64url = /^(?=(?:.{4})*$)[A-Za-z0-9_-]*(?:[AQgw]==|[AEIMQUYcgkosw048]=)?$/; 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 * Prints a warning message followed by a stack trace (to make it easier to figure out what code
* is using the deprecated function). * is using the deprecated function).
@ -107,25 +109,32 @@ const padutils = {
* @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no * @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no
* logger is set), with a stack trace appended if available. * logger is set), with a stack trace appended if available.
*/ */
warnDeprecated: (...args) => { warnDeprecated: (...args) => {
if (padutils.warnDeprecated.disabledForTestingOnly) return; if ("disabledForTestingOnly" in padutils.warnDeprecated) return;
const err = new Error(); const err = new Error();
if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnDeprecated); if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnDeprecated);
err.name = ''; err.name = '';
// Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam. // Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam.
if (typeof err.stack === 'string') { if (typeof err.stack === 'string' && "_rl" in padutils.warnDeprecated) {
if (padutils.warnDeprecated._rl == null) {
padutils.warnDeprecated._rl = padutils.warnDeprecated._rl =
{prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000}; {prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000};
}
const rl = padutils.warnDeprecated._rl; const rl = padutils.warnDeprecated._rl;
const now = rl.now(); if (rl instanceof Object && "now" in rl && "prevs" in rl && "period" in rl
const prev = rl.prevs.get(err.stack); && rl.now instanceof Function && rl.prevs instanceof Map && rl.period instanceof Number){
if (prev != null && now - prev < rl.period) return; const now = rl.now();
rl.prevs.set(err.stack, 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); 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)), 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 * Returns a string that can be used in the `token` cookie as a secret that authenticates a
* particular author. * particular author.
*/ */
generateAuthorToken: () => `t.${randomString()}`, generateAuthorToken: () => {
const randomAuthToken = randomString()
return `t.+${randomAuthToken}`
}
}; };
let globalExceptionHandler = null; let globalExceptionHandler = null;
@ -400,6 +413,8 @@ padutils.setupGlobalExceptionHandler = () => {
.append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')), .append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')),
]; ];
//FIXME gritter not defined
// @ts-ignore
$.gritter.add({ $.gritter.add({
title: 'An error occurred', title: 'An error occurred',
text: errorMsg, 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 // https://stackoverflow.com/a/42660748
const inThirdPartyIframe = () => { const inThirdPartyIframe = () => {
@ -439,11 +454,12 @@ const inThirdPartyIframe = () => {
return true; 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 // This file is included from Node so that it can reuse randomString, but Node doesn't have a global
// window object. // window object.
if (typeof window !== 'undefined') { 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=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 // 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 // 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:', secure: window.location.protocol === 'https:',
}); });
} }
exports.randomString = randomString;
exports.padutils = padutils;

View File

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