Added esm to node directory.

This commit is contained in:
SamTV12345 2023-07-21 19:25:54 +02:00
parent b55a9a2411
commit 6fe4473e25
26 changed files with 519 additions and 456 deletions

View file

@ -4,6 +4,9 @@
require('eslint-config-etherpad/patch/modern-module-resolution'); require('eslint-config-etherpad/patch/modern-module-resolution');
module.exports = { module.exports = {
"parserOptions": {
"sourceType": "module",
},
ignorePatterns: [ ignorePatterns: [
'/static/js/admin/jquery.autosize.js', '/static/js/admin/jquery.autosize.js',
'/static/js/admin/minify.json.js', '/static/js/admin/minify.json.js',

View file

@ -19,19 +19,19 @@
* limitations under the License. * limitations under the License.
*/ */
const Changeset = require('../../static/js/Changeset'); import Changeset from '../../static/js/Changeset'
const ChatMessage = require('../../static/js/ChatMessage'); import ChatMessage from '../../static/js/ChatMessage'
const CustomError = require('../utils/customError'); import CustomError from '../utils/customError'
const padManager = require('./PadManager'); import padManager from './PadManager'
const padMessageHandler = require('../handler/PadMessageHandler'); import padMessageHandler from '../handler/PadMessageHandler'
const readOnlyManager = require('./ReadOnlyManager'); import readOnlyManager from './ReadOnlyManager'
const groupManager = require('./GroupManager'); import groupManager from './GroupManager'
const authorManager = require('./AuthorManager'); import authorManager from './AuthorManager'
const sessionManager = require('./SessionManager'); import sessionManager from './SessionManager'
const exportHtml = require('../utils/ExportHtml'); import exportHtml from '../utils/ExportHtml'
const exportTxt = require('../utils/ExportTxt'); import exportTxt from '../utils/ExportTxt'
const importHtml = require('../utils/ImportHtml'); import {setPadHTML} from '../utils/ImportHtml'
const cleanText = require('./Pad').cleanText; import {cleanText} from './Pad'
const PadDiff = require('../utils/padDiff'); const PadDiff = require('../utils/padDiff');
const { checkValidRev, isInt } = require('../utils/checkValidRev'); const { checkValidRev, isInt } = require('../utils/checkValidRev');
@ -39,39 +39,39 @@ const { checkValidRev, isInt } = require('../utils/checkValidRev');
* GROUP FUNCTIONS **** * GROUP FUNCTIONS ****
******************** */ ******************** */
exports.listAllGroups = groupManager.listAllGroups; export const listAllGroups = groupManager.listAllGroups;
exports.createGroup = groupManager.createGroup; export const createGroup = groupManager.createGroup;
exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor; export const createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor;
exports.deleteGroup = groupManager.deleteGroup; export const deleteGroup = groupManager.deleteGroup;
exports.listPads = groupManager.listPads; export const listPads = groupManager.listPads;
exports.createGroupPad = groupManager.createGroupPad; export const createGroupPad = groupManager.createGroupPad;
/* ******************** /* ********************
* PADLIST FUNCTION *** * PADLIST FUNCTION ***
******************** */ ******************** */
exports.listAllPads = padManager.listAllPads; export const listAllPads = padManager.listAllPads;
/* ******************** /* ********************
* AUTHOR FUNCTIONS *** * AUTHOR FUNCTIONS ***
******************** */ ******************** */
exports.createAuthor = authorManager.createAuthor; export const createAuthor = authorManager.createAuthor;
exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor; export const createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor;
exports.getAuthorName = authorManager.getAuthorName; export const getAuthorName = authorManager.getAuthorName;
exports.listPadsOfAuthor = authorManager.listPadsOfAuthor; export const listPadsOfAuthor = authorManager.listPadsOfAuthor;
exports.padUsers = padMessageHandler.padUsers; export const padUsers = padMessageHandler.padUsers;
exports.padUsersCount = padMessageHandler.padUsersCount; export const padUsersCount = padMessageHandler.padUsersCount;
/* ******************** /* ********************
* SESSION FUNCTIONS ** * SESSION FUNCTIONS **
******************** */ ******************** */
exports.createSession = sessionManager.createSession; export const createSession = sessionManager.createSession;
exports.deleteSession = sessionManager.deleteSession; export const deleteSession = sessionManager.deleteSession;
exports.getSessionInfo = sessionManager.getSessionInfo; export const getSessionInfo = sessionManager.getSessionInfo;
exports.listSessionsOfGroup = sessionManager.listSessionsOfGroup; export const listSessionsOfGroup = sessionManager.listSessionsOfGroup;
exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor; export const listSessionsOfAuthor = sessionManager.listSessionsOfAuthor;
/* *********************** /* ***********************
* PAD CONTENT FUNCTIONS * * PAD CONTENT FUNCTIONS *
@ -277,7 +277,7 @@ exports.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

@ -1,4 +1,3 @@
'use strict';
/** /**
* The AuthorManager controlls all information about the Pad authors * The AuthorManager controlls all information about the Pad authors
*/ */
@ -19,12 +18,15 @@
* limitations under the License. * limitations under the License.
*/ */
const db = require('./DB'); import db from './DB.js';
const CustomError = require('../utils/customError');
const hooks = require('../../static/js/pluginfw/hooks.js');
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
exports.getColorPalette = () => [ import CustomError from '../utils/customError.js';
import {randomString} from '../../static/js/pad_utils.js';
import hooks from '../../static/js/pluginfw/hooks.js';
export const getColorPalette = () => [
'#ffc7c7', '#ffc7c7',
'#fff1c7', '#fff1c7',
'#e3ffc7', '#e3ffc7',
@ -94,23 +96,23 @@ exports.getColorPalette = () => [
/** /**
* Checks if the author exists * Checks if the author exists
*/ */
exports.doesAuthorExist = async (authorID) => { export const doesAuthorExist = async (authorID) => {
const author = await db.get(`globalAuthor:${authorID}`); const author = await db.get(`globalAuthor:${authorID}`);
return author != null; return author != null;
}; };
/* exported for backwards compatibility */ /* exported for backwards compatibility */
exports.doesAuthorExists = exports.doesAuthorExist; export const doesAuthorExists = doesAuthorExist;
const getAuthor4Token = async (token) => { const getAuthor4TokenInt = async (token) => {
const author = await mapAuthorWithDBKey('token2author', token); const author = await mapAuthorWithDBKey('token2author', token);
// return only the sub value authorID // return only the sub value authorID
return author ? author.authorID : author; return author ? author.authorID : author;
}; };
exports.getAuthorId = async (token, user) => { export const getAuthorId = async (token, user) => {
const context = {dbKey: token, token, user}; const context = {dbKey: token, token, user};
let [authorId] = await hooks.aCallFirst('getAuthorId', context); let [authorId] = await hooks.aCallFirst('getAuthorId', context);
if (!authorId) authorId = await getAuthor4Token(context.dbKey); if (!authorId) authorId = await getAuthor4Token(context.dbKey);
@ -123,10 +125,10 @@ exports.getAuthorId = async (token, user) => {
* @deprecated Use `getAuthorId` instead. * @deprecated Use `getAuthorId` instead.
* @param {String} token The token * @param {String} token The token
*/ */
exports.getAuthor4Token = async (token) => { export const getAuthor4Token = async (token) => {
warnDeprecated( warnDeprecated(
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead'); 'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
return await getAuthor4Token(token); return await getAuthor4TokenInt(token);
}; };
/** /**
@ -134,12 +136,12 @@ exports.getAuthor4Token = async (token) => {
* @param {String} token The mapper * @param {String} token The mapper
* @param {String} name The name of the author (optional) * @param {String} name The name of the author (optional)
*/ */
exports.createAuthorIfNotExistsFor = async (authorMapper, name) => { export const createAuthorIfNotExistsFor = async (authorMapper, name) => {
const author = await mapAuthorWithDBKey('mapper2author', authorMapper); const author = await mapAuthorWithDBKey('mapper2author', authorMapper);
if (name) { if (name) {
// set the name of this author // set the name of this author
await exports.setAuthorName(author.authorID, name); await setAuthorName(author.authorID, name);
} }
return author; return author;
@ -157,7 +159,7 @@ const mapAuthorWithDBKey = async (mapperkey, mapper) => {
if (author == null) { if (author == null) {
// there is no author with this mapper, so create one // there is no author with this mapper, so create one
const author = await exports.createAuthor(null); const author = await createAuthor(null);
// create the token2author relation // create the token2author relation
await db.set(`${mapperkey}:${mapper}`, author.authorID); await db.set(`${mapperkey}:${mapper}`, author.authorID);
@ -178,7 +180,7 @@ const mapAuthorWithDBKey = async (mapperkey, mapper) => {
* Internal function that creates the database entry for an author * Internal function that creates the database entry for an author
* @param {String} name The name of the author * @param {String} name The name of the author
*/ */
exports.createAuthor = async (name) => { export const createAuthor = async (name) => {
// create the new author name // create the new author name
const author = `a.${randomString(16)}`; const author = `a.${randomString(16)}`;
@ -199,41 +201,41 @@ exports.createAuthor = async (name) => {
* Returns the Author Obj of the author * Returns the Author Obj of the author
* @param {String} author The id of the author * @param {String} author The id of the author
*/ */
exports.getAuthor = async (author) => await db.get(`globalAuthor:${author}`); export const getAuthor = async (author) => await db.get(`globalAuthor:${author}`);
/** /**
* Returns the color Id of the author * Returns the color Id of the author
* @param {String} author The id of the author * @param {String} author The id of the author
*/ */
exports.getAuthorColorId = async (author) => await db.getSub(`globalAuthor:${author}`, ['colorId']); export const getAuthorColorId = async (author) => await db.getSub(`globalAuthor:${author}`, ['colorId']);
/** /**
* Sets the color Id of the author * Sets the color Id of the author
* @param {String} author The id of the author * @param {String} author The id of the author
* @param {String} colorId The color id of the author * @param {String} colorId The color id of the author
*/ */
exports.setAuthorColorId = async (author, colorId) => await db.setSub( export const setAuthorColorId = async (author, colorId) => await db.setSub(
`globalAuthor:${author}`, ['colorId'], colorId); `globalAuthor:${author}`, ['colorId'], colorId);
/** /**
* Returns the name of the author * Returns the name of the author
* @param {String} author The id of the author * @param {String} author The id of the author
*/ */
exports.getAuthorName = async (author) => await db.getSub(`globalAuthor:${author}`, ['name']); export const getAuthorName = async (author) => await db.getSub(`globalAuthor:${author}`, ['name']);
/** /**
* Sets the name of the author * Sets the name of the author
* @param {String} author The id of the author * @param {String} author The id of the author
* @param {String} name The name of the author * @param {String} name The name of the author
*/ */
exports.setAuthorName = async (author, name) => await db.setSub( export const setAuthorName = async (author, name) => await db.setSub(
`globalAuthor:${author}`, ['name'], name); `globalAuthor:${author}`, ['name'], name);
/** /**
* Returns an array of all pads this author contributed to * Returns an array of all pads this author contributed to
* @param {String} author The id of the author * @param {String} author The id of the author
*/ */
exports.listPadsOfAuthor = async (authorID) => { export const listPadsOfAuthor = async (authorID) => {
/* There are two other places where this array is manipulated: /* There are two other places where this array is manipulated:
* (1) When the author is added to a pad, the author object is also updated * (1) When the author is added to a pad, the author object is also updated
* (2) When a pad is deleted, each author of that pad is also updated * (2) When a pad is deleted, each author of that pad is also updated
@ -258,7 +260,7 @@ exports.listPadsOfAuthor = async (authorID) => {
* @param {String} author The id of the author * @param {String} author 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
*/ */
exports.addPad = async (authorID, padID) => { export const addPad = async (authorID, padID) => {
// get the entry // get the entry
const author = await db.get(`globalAuthor:${authorID}`); const author = await db.get(`globalAuthor:${authorID}`);
@ -285,7 +287,7 @@ exports.addPad = async (authorID, padID) => {
* @param {String} author The id of the author * @param {String} author 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
*/ */
exports.removePad = async (authorID, padID) => { export const removePad = async (authorID, padID) => {
const author = await db.get(`globalAuthor:${authorID}`); const author = await db.get(`globalAuthor:${authorID}`);
if (author == null) return; if (author == null) return;

View file

@ -1,5 +1,3 @@
'use strict';
/** /**
* The DB Module provides a database initialized with the settings * The DB Module provides a database initialized with the settings
* provided by the settings module * provided by the settings module
@ -21,40 +19,45 @@
* limitations under the License. * limitations under the License.
*/ */
const ueberDB = require('ueberdb2'); import ueberDB from 'ueberdb2';
const settings = require('../utils/Settings');
const log4js = require('log4js'); import {dbSettings, dbType} from '../utils/Settings.js';
const stats = require('../stats');
import log4js from 'log4js';
import stats from '../stats.js';
const logger = log4js.getLogger('ueberDB'); const logger = log4js.getLogger('ueberDB');
/** /**
* The UeberDB Object that provides the database functions * The UeberDB Object that provides the database functions
*/ */
exports.db = null; export let db = null;
/** /**
* Initializes the database with the settings provided by the settings module * Initializes the database with the settings provided by the settings module
*/ */
exports.init = async () => { export const init = async () => {
exports.db = new ueberDB.Database(settings.dbType, settings.dbSettings, null, logger); db = new ueberDB.Database(dbType, dbSettings, null, logger);
await exports.db.init(); await db.init();
if (exports.db.metrics != null) { if (db.metrics != null) {
for (const [metric, value] of Object.entries(exports.db.metrics)) { for (const [metric, value] of Object.entries(db.metrics)) {
if (typeof value !== 'number') continue; if (typeof value !== 'number') continue;
stats.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]); stats.gauge(`ueberdb_${metric}`, () => db.metrics[metric]);
} }
} }
for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) { for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) {
const f = exports.db[fn]; const f = db[fn];
exports[fn] = async (...args) => await f.call(exports.db, ...args); global[fn] = async (...args) => await f.call(db, ...args);
Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f)); Object.setPrototypeOf(global[fn], Object.getPrototypeOf(f));
Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f)); Object.defineProperties(global[fn], Object.getOwnPropertyDescriptors(f));
} }
}; };
exports.shutdown = async (hookName, context) => { export const shutdown = async (hookName, context) => {
if (exports.db != null) await exports.db.close(); if (db != null) await db.close();
exports.db = null; db = null;
logger.log('Database closed'); logger.log('Database closed');
}; };
export default db;

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* The ReadOnlyManager manages the database and rendering releated to read only pads * The ReadOnlyManager manages the database and rendering releated to read only pads
*/ */
@ -20,21 +19,21 @@
*/ */
const db = require('./DB'); import {db} from './DB.js';
const randomString = require('../utils/randomstring');
import randomString from '../utils/randomstring.js';
/** /**
* checks if the id pattern matches a read-only pad id * checks if the id pattern matches a read-only pad id
* @param {String} the pad's id * @param {String} the pad's id
*/ */
exports.isReadOnlyId = (id) => id.startsWith('r.'); export const isReadOnlyId = (id) => id.startsWith('r.');
/** /**
* returns a read only id for a pad * returns a read only id for a pad
* @param {String} padId the id of the pad * @param {String} padId the id of the pad
*/ */
exports.getReadOnlyId = async (padId) => { export const getReadOnlyId = async (padId) => {
// check if there is a pad2readonly entry // check if there is a pad2readonly entry
let readOnlyId = await db.get(`pad2readonly:${padId}`); let readOnlyId = await db.get(`pad2readonly:${padId}`);
@ -54,18 +53,18 @@ exports.getReadOnlyId = async (padId) => {
* returns the padId for a read only id * returns the padId for a read only id
* @param {String} readOnlyId read only id * @param {String} readOnlyId read only id
*/ */
exports.getPadId = async (readOnlyId) => await db.get(`readonly2pad:${readOnlyId}`); export const getPadId = async (readOnlyId) => await db.get(`readonly2pad:${readOnlyId}`);
/** /**
* returns the padId and readonlyPadId in an object for any id * returns the padId and readonlyPadId in an object for any id
* @param {String} padIdOrReadonlyPadId read only id or real pad id * @param {String} padIdOrReadonlyPadId read only id or real pad id
*/ */
exports.getIds = async (id) => { export const getIds = async (id) => {
const readonly = exports.isReadOnlyId(id); const readonly = isReadOnlyId(id);
// Might be null, if this is an unknown read-only id // Might be null, if this is an unknown read-only id
const readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id); const readOnlyPadId = readonly ? id : await getReadOnlyId(id);
const padId = readonly ? await exports.getPadId(id) : id; const padId = readonly ? await getPadId(id) : id;
return {readOnlyPadId, padId, readonly}; return {readOnlyPadId, padId, readonly};
}; };

View file

@ -1,13 +1,14 @@
'use strict'; import DB from './DB.js';
const DB = require('./DB'); import {Store} from 'express-session';
const Store = require('express-session').Store;
const log4js = require('log4js'); import log4js from 'log4js';
const util = require('util');
import util from 'util';
const logger = log4js.getLogger('SessionStore'); const logger = log4js.getLogger('SessionStore');
class SessionStore extends Store { export default class SessionStore extends Store {
/** /**
* @param {?number} [refresh] - How often (in milliseconds) `touch()` will update a session's * @param {?number} [refresh] - How often (in milliseconds) `touch()` will update a session's
* database record with the cookie's latest expiration time. If the difference between the * database record with the cookie's latest expiration time. If the difference between the
@ -110,4 +111,3 @@ for (const m of ['get', 'set', 'destroy', 'touch']) {
SessionStore.prototype[m] = util.callbackify(SessionStore.prototype[`_${m}`]); SessionStore.prototype[m] = util.callbackify(SessionStore.prototype[`_${m}`]);
} }
module.exports = SessionStore;

View file

@ -1,19 +1,34 @@
'use strict'; import _ from 'underscore';
const _ = require('underscore'); import * as SecretRotator from '../security/SecretRotator.js';
const SecretRotator = require('../security/SecretRotator');
const cookieParser = require('cookie-parser'); import cookieParser from 'cookie-parser';
const events = require('events');
const express = require('express'); import events from 'events';
const expressSession = require('express-session');
const fs = require('fs'); import express from 'express';
const hooks = require('../../static/js/pluginfw/hooks');
const log4js = require('log4js'); import expressSession from 'express-session';
const SessionStore = require('../db/SessionStore');
const settings = require('../utils/Settings'); import fs from 'fs';
const stats = require('../stats');
const util = require('util'); import * as hooks from '../../static/js/pluginfw/hooks.js';
const webaccess = require('./express/webaccess');
import log4js from 'log4js';
import * as SessionStore from '../db/SessionStore.js';
import * as settings from '../utils/Settings.js';
import stats from '../stats.js';
import util from 'util';
import * as webaccess from './express/webaccess.js';
import https from 'https';
import http from 'http';
let secretRotator = null; let secretRotator = null;
const logger = log4js.getLogger('http'); const logger = log4js.getLogger('http');
@ -23,14 +38,14 @@ const sockets = new Set();
const socketsEvents = new events.EventEmitter(); const socketsEvents = new events.EventEmitter();
const startTime = stats.settableGauge('httpStartTime'); const startTime = stats.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
@ -49,7 +64,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');
} }
@ -59,14 +74,14 @@ const closeServer = async () => {
secretRotator = null; secretRotator = 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 ${settings.getGitCommit()} (https://etherpad.org)`;
console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`);
await exports.restartServer(); await restartServer();
if (settings.ip === '') { if (settings.ip === '') {
// using Unix socket for connectivity // using Unix socket for connectivity
@ -91,7 +106,7 @@ 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
@ -113,12 +128,9 @@ exports.restartServer = async () => {
options.ca.push(fs.readFileSync(caFileName)); options.ca.push(fs.readFileSync(caFileName));
} }
} }
server = https.createServer(options, app);
const https = require('https');
exports.server = https.createServer(options, app);
} else { } else {
const http = require('http'); server = http.createServer(app);
exports.server = http.createServer(app);
} }
app.use((req, res, next) => { app.use((req, res, next) => {
@ -191,7 +203,7 @@ exports.restartServer = async () => {
app.use(cookieParser(secret, {})); app.use(cookieParser(secret, {}));
sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval); sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval);
exports.sessionMiddleware = expressSession({ sessionMiddleware = expressSession({
propagateTouch: true, propagateTouch: true,
rolling: true, rolling: true,
secret, secret,
@ -229,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(webaccess.checkAccess);
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}),
]); ]);
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', () => {
@ -245,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)(settings.port, settings.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

@ -1,23 +1,27 @@
'use strict'; import {strict as assert} from 'assert';
import log4js from 'log4js';
import * as settings from '../../utils/Settings.js';
import {deprecationNotices} from '../../../static/js/pluginfw/hooks.js';
import * as hooks from '../../../static/js/pluginfw/hooks.js';
import * as readOnlyManager from '../../db/ReadOnlyManager.js';
const assert = require('assert').strict;
const log4js = require('log4js');
const httpLogger = log4js.getLogger('http'); const httpLogger = log4js.getLogger('http');
const settings = require('../../utils/Settings'); deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
const hooks = require('../../../static/js/pluginfw/hooks');
const readOnlyManager = require('../../db/ReadOnlyManager');
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
// Promisified wrapper around hooks.aCallFirst. // Promisified wrapper around hooks.aCallFirst.
const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, reject) => { const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, reject) => {
hooks.aCallFirst(hookName, context, (err, r) => err != null ? reject(err) : resolve(r), pred); hooks.aCallFirst(hookName, context, (err, r) => err != null ? reject(err) : resolve(r), pred);
}); });
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 +36,20 @@ exports.normalizeAuthzLevel = (level) => {
return false; return false;
}; };
exports.userCanModify = (padId, req) => { export const userCanModify = (padId, req) => {
if (readOnlyManager.isReadOnlyId(padId)) return false; if (readOnlyManager.isReadOnlyId(padId)) return false;
if (!settings.requireAuthentication) return true; if (!settings.requireAuthentication) return true;
const {session: {user} = {}} = req; const {session: {user} = {}} = 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) => { const checkAccessInternal = async (req, res, next) => {
const requireAdmin = req.path.toLowerCase().startsWith('/admin'); const requireAdmin = req.path.toLowerCase().startsWith('/admin');
// /////////////////////////////////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////////////////////////////////
@ -88,7 +92,7 @@ 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.
@ -131,7 +135,8 @@ const checkAccess = async (req, res, next) => {
// page). // page).
// /////////////////////////////////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////////////////////////////////
if (settings.users == null) settings.users = {}; // FIXME Not necessary
// if (settings.users == null) settings.users = {};
const ctx = {req, res, users: settings.users, next}; const ctx = {req, res, users: settings.users, next};
// 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.
@ -159,7 +164,7 @@ 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;
} }
@ -193,6 +198,6 @@ 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) => { export const checkAccess = (req, res, next) => {
checkAccess(req, res, next).catch((err) => next(err || new Error(err))); checkAccessInternal(req, res, next).catch((err) => next(err || new Error(err)));
}; };

View file

@ -1,9 +1,10 @@
'use strict'; import {Buffer} from 'buffer';
const {Buffer} = require('buffer'); import {hkdf,randomBytes} from './crypto.js';
const crypto = require('./crypto');
const db = require('../db/DB'); import * as db from '../db/DB.js';
const log4js = require('log4js');
import log4js from 'log4js';
class Kdf { class Kdf {
async generateParams() { throw new Error('not implemented'); } async generateParams() { throw new Error('not implemented'); }
@ -23,15 +24,15 @@ class Hkdf extends Kdf {
async generateParams() { async generateParams() {
const [secret, salt] = (await Promise.all([ const [secret, salt] = (await Promise.all([
crypto.randomBytes(this._keyLen), randomBytes(this._keyLen),
crypto.randomBytes(this._keyLen), randomBytes(this._keyLen),
])).map((b) => b.toString('hex')); ])).map((b) => b.toString('hex'));
return {digest: this._digest, keyLen: this._keyLen, salt, secret}; return {digest: this._digest, keyLen: this._keyLen, salt, secret};
} }
async derive(p, info) { async derive(p, info) {
return Buffer.from( return Buffer.from(
await crypto.hkdf(p.digest, p.secret, p.salt, info, p.keyLen)).toString('hex'); await hkdf(p.digest, p.secret, p.salt, info, p.keyLen)).toString('hex');
} }
} }
@ -58,7 +59,7 @@ const intervalStart = (t, interval) => t - mod(t, interval);
* The secrets are generated using a key derivation function (KDF) with input keying material coming * The secrets are generated using a key derivation function (KDF) with input keying material coming
* from a long-lived secret stored in the database (generated if missing). * from a long-lived secret stored in the database (generated if missing).
*/ */
class SecretRotator { export default class SecretRotator {
/** /**
* @param {string} dbPrefix - Database key prefix to use for tracking secret metadata. * @param {string} dbPrefix - Database key prefix to use for tracking secret metadata.
* @param {number} interval - How often to rotate in a new secret. * @param {number} interval - How often to rotate in a new secret.
@ -97,7 +98,7 @@ class SecretRotator {
async _publish(params, id = null) { async _publish(params, id = null) {
// Params are published to the db with a randomly generated key to avoid race conditions with // Params are published to the db with a randomly generated key to avoid race conditions with
// other instances. // other instances.
if (id == null) id = `${this._dbPrefix}:${(await crypto.randomBytes(32)).toString('hex')}`; if (id == null) id = `${this._dbPrefix}:${(await randomBytes(32)).toString('hex')}`;
await db.set(id, params); await db.set(id, params);
return id; return id;
} }
@ -247,5 +248,3 @@ class SecretRotator {
this._t.setTimeout(async () => await this._update(), next - this._t.now()); this._t.setTimeout(async () => await this._update(), next - this._t.now());
} }
} }
module.exports = SecretRotator;

View file

@ -1,15 +1,13 @@
'use strict'; import crypto from 'crypto';
const crypto = require('crypto');
const util = require('util');
import util from 'util';
/** /**
* Promisified version of Node.js's crypto.hkdf. * Promisified version of Node.js's crypto.hkdf.
*/ */
exports.hkdf = util.promisify(crypto.hkdf); export const hkdf = util.promisify(crypto.hkdf);
/** /**
* Promisified version of Node.js's crypto.randomBytes * Promisified version of Node.js's crypto.randomBytes
*/ */
exports.randomBytes = util.promisify(crypto.randomBytes); export const randomBytes = util.promisify(crypto.randomBytes);

View file

@ -1,6 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict';
/** /**
* This module is started with src/bin/run.sh. It sets up a Express HTTP and a Socket.IO Server. * This module is started with src/bin/run.sh. It sets up a Express HTTP and a Socket.IO Server.
@ -24,35 +23,47 @@
* limitations under the License. * limitations under the License.
*/ */
const log4js = require('log4js');
import log4js from 'log4js';
log4js.replaceConsole(); log4js.replaceConsole();
import * as settings from './utils/Settings.js';
import {dumpOnUncleanExit} from './utils/Settings.js';
const settings = require('./utils/Settings');
let wtfnode; import wtfnode0 from 'wtfnode';
if (settings.dumpOnUncleanExit) {
// wtfnode should be loaded after log4js.replaceConsole() so that it uses log4js for logging, and import {check} from './utils/UpdateCheck.js';
// it should be above everything else so that it can hook in before resources are used.
wtfnode = require('wtfnode'); import * as db from './db/DB.js';
}
import * as express from './hooks/express.js';
import * as hooks from '../static/js/pluginfw/hooks.js';
import {plugins as pluginDefs} from '../static/js/pluginfw/plugin_defs.js';
import * as plugins from '../static/js/pluginfw/plugins.js';
import {Gate} from './utils/promises.js';
import stats from './stats.js';
/* /*
* early check for version compatibility before calling * early check for version compatibility before calling
* any modules that require newer versions of NodeJS * any modules that require newer versions of NodeJS
*/ */
const NodeVersion = require('./utils/NodeVersion'); import {enforceMinNodeVersion, checkDeprecationStatus} from './utils/NodeVersion.js';
NodeVersion.enforceMinNodeVersion('12.17.0');
NodeVersion.checkDeprecationStatus('12.17.0', '1.9.0');
const UpdateCheck = require('./utils/UpdateCheck');
const db = require('./db/DB');
const express = require('./hooks/express');
const hooks = require('../static/js/pluginfw/hooks');
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
const plugins = require('../static/js/pluginfw/plugins');
const {Gate} = require('./utils/promises');
const stats = require('./stats');
let wtfnode;
if (dumpOnUncleanExit) {
// wtfnode should be loaded after log4js.replaceConsole() so that it uses log4js for logging, and
// it should be above everything else so that it can hook in before resources are used.
wtfnode = wtfnode0;
}
enforceMinNodeVersion('12.17.0');
checkDeprecationStatus('12.17.0', '1.9.0');
const logger = log4js.getLogger('server'); const logger = log4js.getLogger('server');
const State = { const State = {
@ -76,14 +87,14 @@ const removeSignalListener = (signal, listener) => {
}; };
let startDoneGate; let startDoneGate;
exports.start = async () => { export const start = async () => {
switch (state) { switch (state) {
case State.INITIAL: case State.INITIAL:
break; break;
case State.STARTING: case State.STARTING:
await startDoneGate; await startDoneGate;
// Retry. Don't fall through because it might have transitioned to STATE_TRANSITION_FAILED. // Retry. Don't fall through because it might have transitioned to STATE_TRANSITION_FAILED.
return await exports.start(); return await start();
case State.RUNNING: case State.RUNNING:
return express.server; return express.server;
case State.STOPPING: case State.STOPPING:
@ -100,7 +111,7 @@ exports.start = async () => {
state = State.STARTING; state = State.STARTING;
try { try {
// Check if Etherpad version is up-to-date // Check if Etherpad version is up-to-date
UpdateCheck.check(); check();
stats.gauge('memoryUsage', () => process.memoryUsage().rss); stats.gauge('memoryUsage', () => process.memoryUsage().rss);
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
@ -109,7 +120,7 @@ exports.start = async () => {
logger.debug(`uncaught exception: ${err.stack || err}`); logger.debug(`uncaught exception: ${err.stack || err}`);
// eslint-disable-next-line promise/no-promise-in-callback // eslint-disable-next-line promise/no-promise-in-callback
exports.exit(err) exit(err)
.catch((err) => { .catch((err) => {
logger.error('Error in process exit', JSON.stringify(err)); logger.error('Error in process exit', JSON.stringify(err));
// eslint-disable-next-line n/no-process-exit // eslint-disable-next-line n/no-process-exit
@ -131,7 +142,7 @@ exports.start = async () => {
for (const listener of process.listeners(signal)) { for (const listener of process.listeners(signal)) {
removeSignalListener(signal, listener); removeSignalListener(signal, listener);
} }
process.on(signal, exports.exit); process.on(signal, exit);
// Prevent signal listeners from being added in the future. // Prevent signal listeners from being added in the future.
process.on('newListener', (event, listener) => { process.on('newListener', (event, listener) => {
if (event !== signal) return; if (event !== signal) return;
@ -141,7 +152,7 @@ exports.start = async () => {
await db.init(); await db.init();
await plugins.update(); await plugins.update();
const installedPlugins = Object.values(pluginDefs.plugins) const installedPlugins = Object.values(pluginDefs)
.filter((plugin) => plugin.package.name !== 'ep_etherpad-lite') .filter((plugin) => plugin.package.name !== 'ep_etherpad-lite')
.map((plugin) => `${plugin.package.name}@${plugin.package.version}`) .map((plugin) => `${plugin.package.name}@${plugin.package.version}`)
.join(', '); .join(', ');
@ -154,7 +165,7 @@ exports.start = async () => {
logger.error('Error occurred while starting Etherpad'); logger.error('Error occurred while starting Etherpad');
state = State.STATE_TRANSITION_FAILED; state = State.STATE_TRANSITION_FAILED;
startDoneGate.resolve(); startDoneGate.resolve();
return await exports.exit(err); return await exit(err);
} }
logger.info('Etherpad is running'); logger.info('Etherpad is running');
@ -166,12 +177,12 @@ exports.start = async () => {
}; };
const stopDoneGate = new Gate(); const stopDoneGate = new Gate();
exports.stop = async () => { export const stop = async () => {
switch (state) { switch (state) {
case State.STARTING: case State.STARTING:
await exports.start(); await start();
// Don't fall through to State.RUNNING in case another caller is also waiting for startup. // Don't fall through to State.RUNNING in case another caller is also waiting for startup.
return await exports.stop(); return await stop();
case State.RUNNING: case State.RUNNING:
break; break;
case State.STOPPING: case State.STOPPING:
@ -201,7 +212,7 @@ exports.stop = async () => {
logger.error('Error occurred while stopping Etherpad'); logger.error('Error occurred while stopping Etherpad');
state = State.STATE_TRANSITION_FAILED; state = State.STATE_TRANSITION_FAILED;
stopDoneGate.resolve(); stopDoneGate.resolve();
return await exports.exit(err); return await exit(err);
} }
logger.info('Etherpad stopped'); logger.info('Etherpad stopped');
state = State.STOPPED; state = State.STOPPED;
@ -210,7 +221,7 @@ exports.stop = async () => {
let exitGate; let exitGate;
let exitCalled = false; let exitCalled = false;
exports.exit = async (err = null) => { export const exit = async (err = null) => {
/* eslint-disable no-process-exit */ /* eslint-disable no-process-exit */
if (err === 'SIGTERM') { if (err === 'SIGTERM') {
// Termination from SIGTERM is not treated as an abnormal termination. // Termination from SIGTERM is not treated as an abnormal termination.
@ -222,6 +233,7 @@ exports.exit = async (err = null) => {
process.exitCode = 1; process.exitCode = 1;
if (exitCalled) { if (exitCalled) {
logger.error('Error occurred while waiting to exit. Forcing an immediate unclean exit...'); logger.error('Error occurred while waiting to exit. Forcing an immediate unclean exit...');
// eslint-disable-next-line n/no-process-exit
process.exit(1); process.exit(1);
} }
} }
@ -231,11 +243,11 @@ exports.exit = async (err = null) => {
case State.STARTING: case State.STARTING:
case State.RUNNING: case State.RUNNING:
case State.STOPPING: case State.STOPPING:
await exports.stop(); await stop();
// Don't fall through to State.STOPPED in case another caller is also waiting for stop(). // Don't fall through to State.STOPPED in case another caller is also waiting for stop().
// Don't pass err to exports.exit() because this err has already been processed. (If err is // Don't pass err to exports.exit() because this err has already been processed. (If err is
// passed again to exit() then exit() will think that a second error occurred while exiting.) // passed again to exit() then exit() will think that a second error occurred while exiting.)
return await exports.exit(); return await exit();
case State.INITIAL: case State.INITIAL:
case State.STOPPED: case State.STOPPED:
case State.STATE_TRANSITION_FAILED: case State.STATE_TRANSITION_FAILED:
@ -267,6 +279,7 @@ exports.exit = async (err = null) => {
} }
logger.error('Forcing an unclean exit...'); logger.error('Forcing an unclean exit...');
// eslint-disable-next-line n/no-process-exit
process.exit(1); process.exit(1);
}, 5000).unref(); }, 5000).unref();
@ -275,4 +288,4 @@ exports.exit = async (err = null) => {
/* eslint-enable no-process-exit */ /* eslint-enable no-process-exit */
}; };
if (require.main === module) exports.start(); start();

View file

@ -1,9 +1,9 @@
'use strict'; import measured from 'measured-core';
const measured = require('measured-core'); export default measured.createCollection();
module.exports = measured.createCollection(); export const shutdown = async (hookName, context) => {
// FIXME Is this correcT?
module.exports.shutdown = async (hookName, context) => { // eslint-disable-next-line n/no-process-exit
module.exports.end(); process.exit(0);
}; };

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* Library for deterministic relative filename expansion for Etherpad. * Library for deterministic relative filename expansion for Etherpad.
*/ */
@ -19,12 +18,20 @@
* limitations under the License. * limitations under the License.
*/ */
const log4js = require('log4js'); import log4js from 'log4js';
const path = require('path');
const _ = require('underscore'); import path from 'path';
import _ from 'underscore';
import findRoot from 'find-root';
import {fileURLToPath} from 'url';
const absPathLogger = log4js.getLogger('AbsolutePaths'); const absPathLogger = log4js.getLogger('AbsolutePaths');
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/* /*
* findEtherpadRoot() computes its value only on first invocation. * findEtherpadRoot() computes its value only on first invocation.
* Subsequent invocations are served from this variable. * Subsequent invocations are served from this variable.
@ -49,6 +56,7 @@ const popIfEndsWith = (stringArray, lastDesiredElements) => {
return false; return false;
} }
// eslint-disable-next-line you-dont-need-lodash-underscore/last
const lastElementsFound = _.last(stringArray, lastDesiredElements.length); const lastElementsFound = _.last(stringArray, lastDesiredElements.length);
if (_.isEqual(lastElementsFound, lastDesiredElements)) { if (_.isEqual(lastElementsFound, lastDesiredElements)) {
@ -75,12 +83,10 @@ 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;
} }
const findRoot = require('find-root');
const foundRoot = findRoot(__dirname); const foundRoot = findRoot(__dirname);
const splitFoundRoot = foundRoot.split(path.sep); const splitFoundRoot = foundRoot.split(path.sep);
@ -106,6 +112,7 @@ exports.findEtherpadRoot = () => {
if (maybeEtherpadRoot === false) { if (maybeEtherpadRoot === false) {
absPathLogger.error('Could not identity Etherpad base path in this ' + absPathLogger.error('Could not identity Etherpad base path in this ' +
`${process.platform} installation in "${foundRoot}"`); `${process.platform} installation in "${foundRoot}"`);
// eslint-disable-next-line n/no-process-exit
process.exit(1); process.exit(1);
} }
@ -118,6 +125,7 @@ exports.findEtherpadRoot = () => {
absPathLogger.error( absPathLogger.error(
`To run, Etherpad has to identify an absolute base path. This is not: "${etherpadRoot}"`); `To run, Etherpad has to identify an absolute base path. This is not: "${etherpadRoot}"`);
// eslint-disable-next-line n/no-process-exit
process.exit(1); process.exit(1);
}; };
@ -131,12 +139,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 +158,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,33 +21,33 @@
*/ */
// An object containing the parsed command-line options // An object containing the parsed command-line options
exports.argv = {}; export const argv = {};
const argv = process.argv.slice(2); const argvProcess = process.argv.slice(2);
let arg, prevArg; let arg, prevArg;
// Loop through args // Loop through args
for (let i = 0; i < argv.length; i++) { for (let i = 0; i < argvProcess.length; i++) {
arg = argv[i]; arg = argvProcess[i];
// Override location of settings.json file // Override location of settings.json file
if (prevArg === '--settings' || prevArg === '-s') { if (prevArg === '--settings' || prevArg === '-s') {
exports.argv.settings = arg; argv.settings = arg;
} }
// Override location of credentials.json file // Override location of credentials.json file
if (prevArg === '--credentials') { if (prevArg === '--credentials') {
exports.argv.credentials = arg; argv.credentials = arg;
} }
// Override location of settings.json file // Override location of settings.json file
if (prevArg === '--sessionkey') { if (prevArg === '--sessionkey') {
exports.argv.sessionkey = arg; argv.sessionkey = arg;
} }
// Override location of settings.json file // Override location of settings.json file
if (prevArg === '--apikey') { if (prevArg === '--apikey') {
exports.argv.apikey = arg; argv.apikey = arg;
} }
prevArg = arg; prevArg = arg;

View file

@ -19,14 +19,14 @@
* limitations under the License. * limitations under the License.
*/ */
const semver = require('semver'); import semver from 'semver';
/** /**
* Quits if Etherpad is not running on a given minimum Node version * Quits if Etherpad is not running on a given minimum Node version
* *
* @param {String} minNodeVersion Minimum required Node version * @param {String} minNodeVersion Minimum required Node version
*/ */
exports.enforceMinNodeVersion = (minNodeVersion) => { export const enforceMinNodeVersion = (minNodeVersion) => {
const currentNodeVersion = process.version; const currentNodeVersion = process.version;
// we cannot use template literals, since we still do not know if we are // we cannot use template literals, since we still do not know if we are
@ -34,6 +34,7 @@ exports.enforceMinNodeVersion = (minNodeVersion) => {
if (semver.lt(currentNodeVersion, minNodeVersion)) { if (semver.lt(currentNodeVersion, minNodeVersion)) {
console.error(`Running Etherpad on Node ${currentNodeVersion} is not supported. ` + console.error(`Running Etherpad on Node ${currentNodeVersion} is not supported. ` +
`Please upgrade at least to Node ${minNodeVersion}`); `Please upgrade at least to Node ${minNodeVersion}`);
// eslint-disable-next-line n/no-process-exit
process.exit(1); process.exit(1);
} }
@ -49,7 +50,7 @@ exports.enforceMinNodeVersion = (minNodeVersion) => {
* @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated * @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated
* Node releases * Node releases
*/ */
exports.checkDeprecationStatus = (lowestNonDeprecatedNodeVersion, epRemovalVersion) => { export const checkDeprecationStatus = (lowestNonDeprecatedNodeVersion, epRemovalVersion) => {
const currentNodeVersion = process.version; const currentNodeVersion = process.version;
if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) { if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) {

View file

@ -1,4 +1,3 @@
'use strict';
/** /**
* The Settings module reads the settings out of settings.json and provides * The Settings module reads the settings out of settings.json and provides
* this information to the other modules * this information to the other modules
@ -27,19 +26,32 @@
* limitations under the License. * limitations under the License.
*/ */
const absolutePaths = require('./AbsolutePaths'); import {findEtherpadRoot, isSubdir, makeAbsolute} from './AbsolutePaths.js';
const deepEqual = require('fast-deep-equal/es6');
const fs = require('fs'); import deepEqual from 'fast-deep-equal/es6/index.js';
const os = require('os');
const path = require('path'); // eslint-disable-next-line n/no-deprecated-api
const argv = require('./Cli').argv; import {readFileSync, lstatSync, writeFileSync, exists, existsSync} from 'fs';
const jsonminify = require('jsonminify');
const log4js = require('log4js'); import os from 'os';
const randomString = require('./randomstring');
import path from 'path';
import {argv} from './Cli.js';
import jsonminify from 'jsonminify';
import log4js from 'log4js';
import randomString from './randomstring.js';
import _ from 'underscore';
import packageFile from '../../package.json' assert {type: 'json'};
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';
const _ = require('underscore');
const logger = log4js.getLogger('settings'); const logger = log4js.getLogger('settings');
// Exported values that settings.json and credentials.json cannot override. // Exported values that settings.json and credentials.json cannot override.
@ -68,16 +80,17 @@ const initLogging = (logLevel, config) => {
initLogging(defaultLogLevel, defaultLogConfig()); initLogging(defaultLogLevel, defaultLogConfig());
/* Root path of the installation */ /* Root path of the installation */
exports.root = absolutePaths.findEtherpadRoot(); export const root = 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}`);
exports.settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json'); export const settingsFilename = makeAbsolute(argv.settings || 'settings.json');
exports.credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'); export const credentialsFilename = makeAbsolute(argv.credentials ||
'credentials.json');
/** /**
* The app title, visible e.g. in the browser window * The app title, visible e.g. in the browser window
*/ */
exports.title = 'Etherpad'; export const title = 'Etherpad';
/** /**
* Pathname of the favicon you want to use. If null, the skin's favicon is * Pathname of the favicon you want to use. If null, the skin's favicon is
@ -85,7 +98,7 @@ exports.title = 'Etherpad';
* is used. If this is a relative path it is interpreted as relative to the * is used. If this is a relative path it is interpreted as relative to the
* Etherpad root directory. * Etherpad root directory.
*/ */
exports.favicon = null; export const favicon = null;
/* /*
* Skin name. * Skin name.
@ -93,37 +106,37 @@ exports.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.
*/ */
exports.skinName = null; export let skinName = null;
exports.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
*/ */
exports.ip = '0.0.0.0'; export const ip = '0.0.0.0';
/** /**
* The Port ep-lite should listen to * The Port ep-lite should listen to
*/ */
exports.port = process.env.PORT || 9001; export const port = process.env.PORT || 9001;
/** /**
* Should we suppress Error messages from being in Pad Contents * Should we suppress Error messages from being in Pad Contents
*/ */
exports.suppressErrorsInPadText = false; export const suppressErrorsInPadText = false;
/** /**
* The SSL signed server key and the Certificate Authority's own certificate * The SSL signed server key and the Certificate Authority's own certificate
* default case: ep-lite does *not* use SSL. A signed server key is not required in this case. * default case: ep-lite does *not* use SSL. A signed server key is not required in this case.
*/ */
exports.ssl = false; export const ssl = false;
/** /**
* socket.io transport methods * socket.io transport methods
**/ **/
exports.socketTransportProtocols = ['xhr-polling', 'jsonp-polling', 'htmlfile']; export const socketTransportProtocols = ['xhr-polling', 'jsonp-polling', 'htmlfile'];
exports.socketIo = { export const socketIo = {
/** /**
* Maximum permitted client message size (in bytes). * Maximum permitted client message size (in bytes).
* *
@ -138,16 +151,16 @@ exports.socketIo = {
/* /*
* The Type of the database * The Type of the database
*/ */
exports.dbType = 'dirty'; 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
*/ */
exports.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
*/ */
exports.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 ' +
@ -159,7 +172,7 @@ exports.defaultPadText = [
/** /**
* The default Pad Settings for a user (Can be overridden by changing the setting * The default Pad Settings for a user (Can be overridden by changing the setting
*/ */
exports.padOptions = { export const padOptions = {
noColors: false, noColors: false,
showControls: true, showControls: true,
showChat: true, showChat: true,
@ -176,7 +189,7 @@ exports.padOptions = {
/** /**
* Whether certain shortcut keys are enabled for a user in the pad * Whether certain shortcut keys are enabled for a user in the pad
*/ */
exports.padShortcutEnabled = { export const padShortcutEnabled = {
altF9: true, altF9: true,
altC: true, altC: true,
delete: true, delete: true,
@ -204,7 +217,7 @@ exports.padShortcutEnabled = {
/** /**
* The toolbar buttons and order. * The toolbar buttons and order.
*/ */
exports.toolbar = { export const toolbar = {
left: [ left: [
['bold', 'italic', 'underline', 'strikethrough'], ['bold', 'italic', 'underline', 'strikethrough'],
['orderedlist', 'unorderedlist', 'indent', 'outdent'], ['orderedlist', 'unorderedlist', 'indent', 'outdent'],
@ -224,92 +237,92 @@ exports.toolbar = {
/** /**
* A flag that requires any user to have a valid session (via the api) before accessing a pad * A flag that requires any user to have a valid session (via the api) before accessing a pad
*/ */
exports.requireSession = false; export const requireSession = false;
/** /**
* A flag that prevents users from creating new pads * A flag that prevents users from creating new pads
*/ */
exports.editOnly = false; export const editOnly = false;
/** /**
* Max age that responses will have (affects caching layer). * Max age that responses will have (affects caching layer).
*/ */
exports.maxAge = 1000 * 60 * 60 * 6; // 6 hours export const maxAge = 1000 * 60 * 60 * 6; // 6 hours
/** /**
* A flag that shows if minification is enabled or not * A flag that shows if minification is enabled or not
*/ */
exports.minify = true; export const minify = true;
/** /**
* The path of the abiword executable * The path of the abiword executable
*/ */
exports.abiword = null; export let abiword = null;
/** /**
* The path of the libreoffice executable * The path of the libreoffice executable
*/ */
exports.soffice = null; export let soffice = null;
/** /**
* The path of the tidy executable * The path of the tidy executable
*/ */
exports.tidyHtml = null; export const tidyHtml = null;
/** /**
* Should we support none natively supported file types on import? * Should we support none natively supported file types on import?
*/ */
exports.allowUnknownFileEnds = true; export const allowUnknownFileEnds = true;
/** /**
* The log level of log4js * The log level of log4js
*/ */
exports.loglevel = defaultLogLevel; export const loglevel = defaultLogLevel;
/** /**
* Disable IP logging * Disable IP logging
*/ */
exports.disableIPlogging = false; export const disableIPlogging = false;
/** /**
* Number of seconds to automatically reconnect pad * Number of seconds to automatically reconnect pad
*/ */
exports.automaticReconnectionTimeout = 0; export const automaticReconnectionTimeout = 0;
/** /**
* Disable Load Testing * Disable Load Testing
*/ */
exports.loadTest = false; export const loadTest = false;
/** /**
* Disable dump of objects preventing a clean exit * Disable dump of objects preventing a clean exit
*/ */
exports.dumpOnUncleanExit = false; export const dumpOnUncleanExit = false;
/** /**
* Enable indentation on new lines * Enable indentation on new lines
*/ */
exports.indentationOnNewLine = true; export const indentationOnNewLine = true;
/* /*
* log4js appender configuration * log4js appender configuration
*/ */
exports.logconfig = defaultLogConfig(); export const logconfig = defaultLogConfig();
/* /*
* Deprecated cookie signing key. * Deprecated cookie signing key.
*/ */
exports.sessionKey = null; export let sessionKey = null;
/* /*
* Trust Proxy, whether or not trust the x-forwarded-for header. * Trust Proxy, whether or not trust the x-forwarded-for header.
*/ */
exports.trustProxy = false; export const trustProxy = false;
/* /*
* Settings controlling the session cookie issued by Etherpad. * Settings controlling the session cookie issued by Etherpad.
*/ */
exports.cookie = { export const cookie = {
keyRotationInterval: 1 * 24 * 60 * 60 * 1000, keyRotationInterval: 1 * 24 * 60 * 60 * 1000,
/* /*
* Value of the SameSite cookie property. "Lax" is recommended unless * Value of the SameSite cookie property. "Lax" is recommended unless
@ -332,20 +345,20 @@ exports.cookie = {
* authorization. Note: /admin always requires authentication, and * authorization. Note: /admin always requires authentication, and
* either authorization by a module, or a user with is_admin set * either authorization by a module, or a user with is_admin set
*/ */
exports.requireAuthentication = false; export const requireAuthentication = false;
exports.requireAuthorization = false; export const requireAuthorization = false;
exports.users = {}; export let users = {};
/* /*
* Show settings in admin page, by default it is true * Show settings in admin page, by default it is true
*/ */
exports.showSettingsInAdminPage = true; export const showSettingsInAdminPage = true;
/* /*
* By default, when caret is moved out of viewport, it scrolls the minimum * By default, when caret is moved out of viewport, it scrolls the minimum
* height needed to make this line visible. * height needed to make this line visible.
*/ */
exports.scrollWhenFocusLineIsOutOfViewport = { export const scrollWhenFocusLineIsOutOfViewport = {
/* /*
* Percentage of viewport height to be additionally scrolled. * Percentage of viewport height to be additionally scrolled.
*/ */
@ -378,12 +391,12 @@ exports.scrollWhenFocusLineIsOutOfViewport = {
* *
* Do not enable on production machines. * Do not enable on production machines.
*/ */
exports.exposeVersion = false; export const exposeVersion = false;
/* /*
* Override any strings found in locale directories * Override any strings found in locale directories
*/ */
exports.customLocaleStrings = {}; export const customLocaleStrings = {};
/* /*
* 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
@ -394,7 +407,7 @@ exports.customLocaleStrings = {};
* *
* See https://github.com/nfriedly/express-rate-limit for more options * See https://github.com/nfriedly/express-rate-limit for more options
*/ */
exports.importExportRateLimiting = { export const importExportRateLimiting = {
// duration of the rate limit window (milliseconds) // duration of the rate limit window (milliseconds)
windowMs: 90000, windowMs: 90000,
@ -410,7 +423,7 @@ exports.importExportRateLimiting = {
* *
* See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options * See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options
*/ */
exports.commitRateLimiting = { export const commitRateLimiting = {
// duration of the rate limit window (seconds) // duration of the rate limit window (seconds)
duration: 1, duration: 1,
@ -424,39 +437,39 @@ exports.commitRateLimiting = {
* *
* File size is specified in bytes. Default is 50 MB. * File size is specified in bytes. Default is 50 MB.
*/ */
exports.importMaxFileSize = 50 * 1024 * 1024; export const importMaxFileSize = 50 * 1024 * 1024;
/* /*
* Disable Admin UI tests * Disable Admin UI tests
*/ */
exports.enableAdminUITests = false; export const enableAdminUITests = false;
/* /*
* Enable auto conversion of pad Ids to lowercase. * Enable auto conversion of pad Ids to lowercase.
* e.g. /p/EtHeRpAd to /p/etherpad * e.g. /p/EtHeRpAd to /p/etherpad
*/ */
exports.lowerCasePadIds = false; export const lowerCasePadIds = false;
// checks if abiword is avaiable // checks if abiword is avaiable
exports.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';
} }
}; };
exports.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';
} }
}; };
exports.exportAvailable = () => { export const exportAvailable = () => {
const abiword = exports.abiwordAvailable(); const abiword = abiwordAvailable();
const soffice = exports.sofficeAvailable(); const soffice = sofficeAvailable();
if (abiword === 'no' && soffice === 'no') { if (abiword === 'no' && soffice === 'no') {
return 'no'; return 'no';
@ -469,20 +482,20 @@ exports.exportAvailable = () => {
}; };
// Provide git version if available // Provide git version if available
exports.getGitCommit = () => { export const getGitCommit = () => {
let version = ''; let version = '';
try { try {
let rootPath = exports.root; let rootPath = root;
if (fs.lstatSync(`${rootPath}/.git`).isFile()) { if (lstatSync(`${rootPath}/.git`).isFile()) {
rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8'); rootPath = readFileSync(`${rootPath}/.git`, 'utf8');
rootPath = rootPath.split(' ').pop().trim(); rootPath = rootPath.split(' ').pop().trim();
} else { } else {
rootPath += '/.git'; rootPath += '/.git';
} }
const ref = fs.readFileSync(`${rootPath}/HEAD`, 'utf-8'); const ref = readFileSync(`${rootPath}/HEAD`, 'utf-8');
if (ref.startsWith('ref: ')) { if (ref.startsWith('ref: ')) {
const refPath = `${rootPath}/${ref.substring(5, ref.indexOf('\n'))}`; const refPath = `${rootPath}/${ref.substring(5, ref.indexOf('\n'))}`;
version = fs.readFileSync(refPath, 'utf-8'); version = readFileSync(refPath, 'utf-8');
} else { } else {
version = ref; version = ref;
} }
@ -494,7 +507,7 @@ exports.getGitCommit = () => {
}; };
// Return etherpad version from package.json // Return etherpad version from package.json
exports.getEpVersion = () => require('../../package.json').version; export const getEpVersion = () => packageFile.version;
/** /**
* Receives a settingsObj and, if the property name is a valid configuration * Receives a settingsObj and, if the property name is a valid configuration
@ -517,11 +530,13 @@ const storeSettings = (settingsObj) => {
// we know this setting, so we overwrite it // we know this setting, so we overwrite it
// or it's a settings hash, specific to a plugin // or it's a settings hash, specific to a plugin
if (exports[i] !== undefined || i.indexOf('ep_') === 0) { // eslint-disable-next-line no-eval
let variable = eval(i);
if (variable !== undefined || i.indexOf('ep_') === 0) {
if (_.isObject(settingsObj[i]) && !Array.isArray(settingsObj[i])) { if (_.isObject(settingsObj[i]) && !Array.isArray(settingsObj[i])) {
exports[i] = _.defaults(settingsObj[i], exports[i]); variable = _.defaults(settingsObj[i], variable);
} else { } else {
exports[i] = settingsObj[i]; variable = settingsObj[i];
} }
} else { } else {
// this setting is unknown, output a warning and throw it away // this setting is unknown, output a warning and throw it away
@ -702,7 +717,7 @@ const parseSettings = (settingsFilename, isSettings) => {
try { try {
// read the settings file // read the settings file
settingsStr = fs.readFileSync(settingsFilename).toString(); settingsStr = readFileSync(settingsFilename).toString();
} catch (e) { } catch (e) {
notFoundFunction(`No ${settingsType} file found in ${settingsFilename}. ${notFoundMessage}`); notFoundFunction(`No ${settingsType} file found in ${settingsFilename}. ${notFoundMessage}`);
@ -717,107 +732,106 @@ const parseSettings = (settingsFilename, isSettings) => {
logger.info(`${settingsType} loaded from: ${settingsFilename}`); logger.info(`${settingsType} loaded from: ${settingsFilename}`);
const replacedSettings = lookupEnvironmentVariables(settings); return lookupEnvironmentVariables(settings);
return replacedSettings;
} catch (e) { } catch (e) {
logger.error(`There was an error processing your ${settingsType} ` + logger.error(`There was an error processing your ${settingsType} ` +
`file from ${settingsFilename}: ${e.message}`); `file from ${settingsFilename}: ${e.message}`);
// eslint-disable-next-line n/no-process-exit
process.exit(1); process.exit(1);
} }
}; };
exports.reloadSettings = () => { export const reloadSettings = () => {
const settings = parseSettings(exports.settingsFilename, true); const settings = parseSettings(settingsFilename, true);
const credentials = parseSettings(exports.credentialsFilename, false); const credentials = parseSettings(credentialsFilename, false);
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 (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 (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) => { exists(abiword, (exists) => {
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) => { exists(soffice, (exists) => {
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;
} }
}); });
} }
const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); const sessionkeyFilename = makeAbsolute(argv.sessionkey || './SESSIONKEY.txt');
if (!exports.sessionKey) { if (!sessionKey) {
try { try {
exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); sessionKey = readFileSync(sessionkeyFilename, 'utf8');
logger.info(`Session key loaded from: ${sessionkeyFilename}`); logger.info(`Session key loaded from: ${sessionkeyFilename}`);
} catch (err) { /* ignored */ } } catch (err) { /* ignored */ }
const keyRotationEnabled = exports.cookie.keyRotationInterval && exports.cookie.sessionLifetime; const keyRotationEnabled = cookie.keyRotationInterval && cookie.sessionLifetime;
if (!exports.sessionKey && !keyRotationEnabled) { if (!sessionKey && !keyRotationEnabled) {
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'); 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,22 +839,22 @@ exports.reloadSettings = () => {
'If you are seeing this error after restarting using the Admin User ' + 'If you are seeing this error after restarting using the Admin User ' +
'Interface then you can ignore this message.'); 'Interface then you can ignore this message.');
} }
if (exports.sessionKey) { if (sessionKey) {
logger.warn(`The sessionKey setting and ${sessionkeyFilename} file are deprecated; ` + logger.warn(`The sessionKey setting and ${sessionkeyFilename} file are deprecated; ` +
'use automatic key rotation instead (see the cookie.keyRotationInterval setting).'); 'use automatic key rotation instead (see the cookie.keyRotationInterval setting).');
} }
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 = 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.');
@ -857,13 +871,13 @@ exports.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
*/ */
exports.randomVersionString = randomString(4); const randomVersionString = randomString(4);
logger.info(`Random string used for versioning assets: ${exports.randomVersionString}`); logger.info(`Random string used for versioning assets: ${randomVersionString}`);
}; };
exports.exportedForTestingOnly = { export const exportedForTestingOnly = {
parseSettings, parseSettings,
}; };
// initially load settings // initially load settings
exports.reloadSettings(); reloadSettings();

View file

@ -1,35 +1,34 @@
'use strict'; import axios from 'axios';
const semver = require('semver');
const settings = require('./Settings');
const axios = require('axios');
let infos;
const loadEtherpadInformations = () => import {getEpVersion} from './Settings.js';
axios.get('https://static.etherpad.org/info.json')
.then(async resp => { import semver from 'semver';
const loadEtherpadInformations = () => axios.get('https://static.etherpad.org/info.json')
.then(async (resp) => {
try { try {
infos = await resp.data; const infos = await resp.data;
if (infos === undefined || infos === null) { if (infos === undefined || infos == null) {
await Promise.reject("Could not retrieve current version") await Promise.reject(new Error('Could not retrieve current version'));
return return;
} }
return await Promise.resolve(infos); return infos;
} catch (err) {
return err;
} }
catch (err) { });
return await Promise.reject(err);
}
})
exports.getLatestVersion = () => { export const getLatestVersion = () => {
exports.needsUpdate(); const infos = needsUpdate();
return infos.latestVersion; return infos;
}; };
exports.needsUpdate = async (cb) => { export const needsUpdate = async (cb) => {
await loadEtherpadInformations() await loadEtherpadInformations()
.then((info) => { .then((info) => {
if (semver.gt(info.latestVersion, settings.getEpVersion())) { if (semver.gt(info.latestVersion, getEpVersion())) {
if (cb) return cb(true); if (cb) return cb(true);
} }
}).catch((err) => { }).catch((err) => {
@ -38,8 +37,8 @@ exports.needsUpdate = async (cb) => {
}); });
}; };
exports.check = () => { export const check = () => {
exports.needsUpdate((needsUpdate) => { needsUpdate((needsUpdate) => {
if (needsUpdate) { if (needsUpdate) {
console.warn(`Update available: Download the actual version ${infos.latestVersion}`); console.warn(`Update available: Download the actual version ${infos.latestVersion}`);
} }

View file

@ -1,4 +1,4 @@
'use strict';
/** /**
* Helpers to manipulate promises (like async but for promises). * Helpers to manipulate promises (like async but for promises).
*/ */
@ -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(() => {
@ -59,7 +59,7 @@ exports.timesLimit = async (total, concurrency, promiseCreator) => {
* 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 { 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 +73,3 @@ class Gate extends Promise {
Object.assign(this, props); Object.assign(this, props);
} }
} }
exports.Gate = Gate;

View file

@ -1,10 +1,9 @@
'use strict';
/** /**
* Generates a random String with the given length. Is needed to generate the * Generates a random String with the given length. Is needed to generate the
* Author, Group, readonly, session Ids * Author, Group, readonly, session Ids
*/ */
const crypto = require('crypto'); import crypto from 'crypto';
const randomString = (len) => crypto.randomBytes(len).toString('hex'); const randomString = (len) => crypto.randomBytes(len).toString('hex');
module.exports = randomString; export default randomString;

View file

@ -1,9 +1,10 @@
'use strict'; import spawn from 'cross-spawn';
const spawn = require('cross-spawn'); import log4js from 'log4js';
const log4js = require('log4js');
const path = require('path'); import path from 'path';
const settings = require('./Settings');
import * as settings from './Settings.js';
const logger = log4js.getLogger('runCmd'); const logger = log4js.getLogger('runCmd');
@ -69,7 +70,7 @@ 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 default (args, opts = {}) => {
logger.debug(`Executing command: ${args.join(' ')}`); logger.debug(`Executing command: ${args.join(' ')}`);
opts = {cwd: settings.root, ...opts}; opts = {cwd: settings.root, ...opts};

View file

@ -2,6 +2,7 @@
"name": "ep_etherpad-lite", "name": "ep_etherpad-lite",
"description": "A free and open source realtime collaborative editor", "description": "A free and open source realtime collaborative editor",
"homepage": "https://etherpad.org", "homepage": "https://etherpad.org",
"type": "module",
"keywords": [ "keywords": [
"etherpad", "etherpad",
"realtime", "realtime",

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import * as pluginDefs from './plugin_defs.js';
const pluginDefs = require('./plugin_defs');
// Maps the name of a server-side hook to a string explaining the deprecation // Maps the name of a server-side hook to a string explaining the deprecation
// (e.g., 'use the foo hook instead'). // (e.g., 'use the foo hook instead').
@ -10,12 +10,12 @@ const pluginDefs = require('./plugin_defs');
// const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); // const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
// hooks.deprecationNotices.fooBar = 'use the newSpiffy hook instead'; // hooks.deprecationNotices.fooBar = 'use the newSpiffy hook instead';
// //
exports.deprecationNotices = {}; export const deprecationNotices = {};
const deprecationWarned = {}; const deprecationWarned = {};
const checkDeprecation = (hook) => { const checkDeprecation = (hook) => {
const notice = exports.deprecationNotices[hook.hook_name]; const notice = deprecationNotices[hook.hook_name];
if (notice == null) return; if (notice == null) return;
if (deprecationWarned[hook.hook_fn_name]) return; if (deprecationWarned[hook.hook_fn_name]) return;
console.warn(`${hook.hook_name} hook used by the ${hook.part.plugin} plugin ` + console.warn(`${hook.hook_name} hook used by the ${hook.part.plugin} plugin ` +
@ -189,7 +189,7 @@ const callHookFnSync = (hook, context) => {
// 1. Collect all values returned by the hook functions into an array. // 1. Collect all values returned by the hook functions into an array.
// 2. Convert each `undefined` entry into `[]`. // 2. Convert each `undefined` entry into `[]`.
// 3. Flatten one level. // 3. Flatten one level.
exports.callAll = (hookName, context) => { export const callAll = (hookName, context) => {
if (context == null) context = {}; if (context == null) context = {};
const hooks = pluginDefs.hooks[hookName] || []; const hooks = pluginDefs.hooks[hookName] || [];
return flatten1(hooks.map((hook) => normalizeValue(callHookFnSync(hook, context)))); return flatten1(hooks.map((hook) => normalizeValue(callHookFnSync(hook, context))));
@ -342,7 +342,7 @@ const callHookFnAsync = async (hook, context) => {
// 2. Convert each `undefined` entry into `[]`. // 2. Convert each `undefined` entry into `[]`.
// 3. Flatten one level. // 3. Flatten one level.
// If cb is non-null, this function resolves to the value returned by cb. // If cb is non-null, this function resolves to the value returned by cb.
exports.aCallAll = async (hookName, context, cb = null) => { export const aCallAll = async (hookName, context, cb = null) => {
if (cb != null) return await attachCallback(exports.aCallAll(hookName, context), cb); if (cb != null) return await attachCallback(exports.aCallAll(hookName, context), cb);
if (context == null) context = {}; if (context == null) context = {};
const hooks = pluginDefs.hooks[hookName] || []; const hooks = pluginDefs.hooks[hookName] || [];
@ -354,7 +354,7 @@ exports.aCallAll = async (hookName, context, cb = null) => {
// Like `aCallAll()` except the hook functions are called one at a time instead of concurrently. // Like `aCallAll()` except the hook functions are called one at a time instead of concurrently.
// Only use this function if the hook functions must be called one at a time, otherwise use // Only use this function if the hook functions must be called one at a time, otherwise use
// `aCallAll()`. // `aCallAll()`.
exports.callAllSerial = async (hookName, context) => { export const callAllSerial = async (hookName, context) => {
if (context == null) context = {}; if (context == null) context = {};
const hooks = pluginDefs.hooks[hookName] || []; const hooks = pluginDefs.hooks[hookName] || [];
const results = []; const results = [];
@ -367,7 +367,7 @@ exports.callAllSerial = async (hookName, context) => {
// DEPRECATED: Use `aCallFirst()` instead. // DEPRECATED: Use `aCallFirst()` instead.
// //
// Like `aCallFirst()`, but synchronous. Hook functions must provide their values synchronously. // Like `aCallFirst()`, but synchronous. Hook functions must provide their values synchronously.
exports.callFirst = (hookName, context) => { export const callFirst = (hookName, context) => {
if (context == null) context = {}; if (context == null) context = {};
const predicate = (val) => val.length; const predicate = (val) => val.length;
const hooks = pluginDefs.hooks[hookName] || []; const hooks = pluginDefs.hooks[hookName] || [];
@ -399,9 +399,9 @@ exports.callFirst = (hookName, context) => {
// If cb is nullish, resolves to an array that is either the normalized value that satisfied the // If cb is nullish, resolves to an array that is either the normalized value that satisfied the
// predicate or empty if the predicate was never satisfied. If cb is non-nullish, resolves to the // predicate or empty if the predicate was never satisfied. If cb is non-nullish, resolves to the
// value returned from cb(). // value returned from cb().
exports.aCallFirst = async (hookName, context, cb = null, predicate = null) => { export const aCallFirst = async (hookName, context, cb = null, predicate = null) => {
if (cb != null) { if (cb != null) {
return await attachCallback(exports.aCallFirst(hookName, context, null, predicate), cb); return await attachCallback(aCallFirst(hookName, context, null, predicate), cb);
} }
if (context == null) context = {}; if (context == null) context = {};
if (predicate == null) predicate = (val) => val.length; if (predicate == null) predicate = (val) => val.length;
@ -413,7 +413,7 @@ exports.aCallFirst = async (hookName, context, cb = null, predicate = null) => {
return []; return [];
}; };
exports.exportedForTestingOnly = { export const exportedForTestingOnly = {
callHookFnAsync, callHookFnAsync,
callHookFnSync, callHookFnSync,
deprecationWarned, deprecationWarned,

View file

@ -8,13 +8,13 @@
// * hook_fn: Plugin-supplied hook function. // * hook_fn: Plugin-supplied hook function.
// * hook_fn_name: Name of the hook function, with the form <filename>:<functionName>. // * hook_fn_name: Name of the hook function, with the form <filename>:<functionName>.
// * part: The ep.json part object that declared the hook. See exports.plugins. // * part: The ep.json part object that declared the hook. See exports.plugins.
exports.hooks = {}; export let hooks = {};
// Whether the plugins have been loaded. // Whether the plugins have been loaded.
exports.loaded = false; export let loaded = false;
// Topologically sorted list of parts from exports.plugins. // Topologically sorted list of parts from exports.plugins.
exports.parts = []; export let parts = [];
// Maps the name of a plugin to the plugin's definition provided in ep.json. The ep.json object is // Maps the name of a plugin to the plugin's definition provided in ep.json. The ep.json object is
// augmented with additional metadata: // augmented with additional metadata:
@ -25,4 +25,11 @@ exports.parts = [];
// - version // - version
// - path // - path
// - realPath // - realPath
exports.plugins = {}; export let plugins = {};
export const setParts = (partsLoaded)=> parts = partsLoaded
export const setPlugins = (pluginsLoaded)=>plugins = pluginsLoaded
export const setHooks = (hooksLoaded)=>hooks = hooksLoaded
export const setLoaded = (loadedNew)=>loaded = loadedNew

View file

@ -1,13 +1,13 @@
'use strict'; 'use strict';
const fs = require('fs').promises; import {promises} from 'fs'
const hooks = require('./hooks'); import * as hooks from './hooks.js'
const log4js = require('log4js'); import log4js from 'log4js'
const path = require('path'); import path from 'path'
const runCmd = require('../../../node/utils/run_cmd'); import runCmd from '../../../node/utils/run_cmd.js'
const tsort = require('./tsort'); import tsort from './tsort.js'
const pluginUtils = require('./shared'); import * as pluginUtils from './shared.js'
const defs = require('./plugin_defs'); import {parts, plugins, loaded, setPlugins, setParts, setHooks, setLoaded} from './plugin_defs.js'
const logger = log4js.getLogger('plugins'); const logger = log4js.getLogger('plugins');
@ -22,15 +22,15 @@ const logger = log4js.getLogger('plugins');
} }
})(); })();
exports.prefix = 'ep_'; export const prefix = 'ep_';
exports.formatPlugins = () => Object.keys(defs.plugins).join(', '); export const formatPlugins = () => Object.keys(plugins).join(', ');
exports.formatParts = () => defs.parts.map((part) => part.full_name).join('\n'); export const formatParts = () => parts.map((part) => part.full_name).join('\n');
exports.formatHooks = (hookSetName, html) => { export const formatHooks = (hookSetName, html) => {
let hooks = new Map(); let hooks = new Map();
for (const [pluginName, def] of Object.entries(defs.plugins)) { for (const [pluginName, def] of Object.entries(plugins)) {
for (const part of def.parts) { for (const part of def.parts) {
for (const [hookName, hookFnName] of Object.entries(part[hookSetName] || {})) { for (const [hookName, hookFnName] of Object.entries(part[hookSetName] || {})) {
let hookEntry = hooks.get(hookName); let hookEntry = hooks.get(hookName);
@ -72,39 +72,39 @@ exports.formatHooks = (hookSetName, html) => {
return lines.join('\n'); return lines.join('\n');
}; };
exports.pathNormalization = (part, hookFnName, hookName) => { export const pathNormalization = (part, hookFnName, hookName) => {
const tmp = hookFnName.split(':'); // hookFnName might be something like 'C:\\foo.js:myFunc'. const tmp = hookFnName.split(':'); // hookFnName might be something like 'C:\\foo.js:myFunc'.
// If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'. // If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'.
const functionName = (tmp.length > 1 ? tmp.pop() : null) || hookName; const functionName = (tmp.length > 1 ? tmp.pop() : null) || hookName;
const moduleName = tmp.join(':') || part.plugin; const moduleName = tmp.join(':') || part.plugin;
const packageDir = path.dirname(defs.plugins[part.plugin].package.path); const packageDir = path.dirname(plugins[part.plugin].package.path);
const fileName = path.join(packageDir, moduleName); const fileName = path.join(packageDir, moduleName);
return `${fileName}:${functionName}`; return `${fileName}:${functionName}`;
}; };
exports.update = async () => { export const update = async () => {
const packages = await exports.getPackages(); const packagesNew = await getPackages();
const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array. let partsNew = {}; // Key is full name. sortParts converts this into a topologically sorted array.
const plugins = {}; let pluginsNew = {};
// Load plugin metadata ep.json // Load plugin metadata ep.json
await Promise.all(Object.keys(packages).map(async (pluginName) => { await Promise.all(Object.keys(packagesNew).map(async (pluginName) => {
logger.info(`Loading plugin ${pluginName}...`); logger.info(`Loading plugin ${pluginName}...`);
await loadPlugin(packages, pluginName, plugins, parts); await loadPlugin(packagesNew, pluginName, pluginsNew, parts);
})); }));
logger.info(`Loaded ${Object.keys(packages).length} plugins`); logger.info(`Loaded ${Object.keys(packagesNew).length} plugins`);
defs.plugins = plugins; setPlugins(pluginsNew)
defs.parts = sortParts(parts); setParts(sortParts(partsNew))
defs.hooks = pluginUtils.extractHooks(defs.parts, 'hooks', exports.pathNormalization); setHooks(pluginUtils.extractHooks(parts, 'hooks', pathNormalization));
defs.loaded = true; setLoaded(true)
await Promise.all(Object.keys(defs.plugins).map(async (p) => { await Promise.all(Object.keys(plugins).map(async (p) => {
const logger = log4js.getLogger(`plugin:${p}`); const logger = log4js.getLogger(`plugin:${p}`);
await hooks.aCallAll(`init_${p}`, {logger}); await hooks.aCallAll(`init_${p}`, {logger});
})); }));
}; };
exports.getPackages = async () => { export const getPackages = async () => {
logger.info('Running npm to get a list of installed plugins...'); logger.info('Running npm to get a list of installed plugins...');
// Notes: // Notes:
// * Do not pass `--prod` otherwise `npm ls` will fail if there is no `package.json`. // * Do not pass `--prod` otherwise `npm ls` will fail if there is no `package.json`.
@ -114,11 +114,11 @@ exports.getPackages = async () => {
const cmd = ['npm', 'ls', '--long', '--json', '--depth=0', '--no-production']; const cmd = ['npm', 'ls', '--long', '--json', '--depth=0', '--no-production'];
const {dependencies = {}} = JSON.parse(await runCmd(cmd, {stdio: [null, 'string']})); const {dependencies = {}} = JSON.parse(await runCmd(cmd, {stdio: [null, 'string']}));
await Promise.all(Object.entries(dependencies).map(async ([pkg, info]) => { await Promise.all(Object.entries(dependencies).map(async ([pkg, info]) => {
if (!pkg.startsWith(exports.prefix)) { if (!pkg.startsWith(prefix)) {
delete dependencies[pkg]; delete dependencies[pkg];
return; return;
} }
info.realPath = await fs.realpath(info.path); info.realPath = await promises.realpath(info.path);
})); }));
return dependencies; return dependencies;
}; };
@ -126,7 +126,7 @@ exports.getPackages = async () => {
const loadPlugin = async (packages, pluginName, plugins, parts) => { const loadPlugin = async (packages, pluginName, plugins, parts) => {
const pluginPath = path.resolve(packages[pluginName].path, 'ep.json'); const pluginPath = path.resolve(packages[pluginName].path, 'ep.json');
try { try {
const data = await fs.readFile(pluginPath); const data = await promises.readFile(pluginPath);
try { try {
const plugin = JSON.parse(data); const plugin = JSON.parse(data);
plugin.package = packages[pluginName]; plugin.package = packages[pluginName];

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const defs = require('./plugin_defs'); import {parts} from './plugin_defs.js'
const disabledHookReasons = { const disabledHookReasons = {
hooks: { hooks: {
@ -33,7 +33,7 @@ const loadFn = (path, hookName) => {
return fn; return fn;
}; };
const extractHooks = (parts, hookSetName, normalizer) => { export const extractHooks = (parts, hookSetName, normalizer) => {
const hooks = {}; const hooks = {};
for (const part of parts) { for (const part of parts) {
for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) { for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) {
@ -74,7 +74,6 @@ const extractHooks = (parts, hookSetName, normalizer) => {
return hooks; return hooks;
}; };
exports.extractHooks = extractHooks;
/* /*
* Returns an array containing the names of the installed client-side plugins * Returns an array containing the names of the installed client-side plugins
@ -88,8 +87,8 @@ exports.extractHooks = extractHooks;
* No plugins: [] * No plugins: []
* Some plugins: [ 'ep_adminpads', 'ep_add_buttons', 'ep_activepads' ] * Some plugins: [ 'ep_adminpads', 'ep_add_buttons', 'ep_activepads' ]
*/ */
exports.clientPluginNames = () => { export const clientPluginNames = () => {
const clientPluginNames = defs.parts const clientPluginNames = parts
.filter((part) => Object.prototype.hasOwnProperty.call(part, 'client_hooks')) .filter((part) => Object.prototype.hasOwnProperty.call(part, 'client_hooks'))
.map((part) => `plugin-${part.plugin}`); .map((part) => `plugin-${part.plugin}`);
return [...new Set(clientPluginNames)]; return [...new Set(clientPluginNames)];

View file

@ -111,3 +111,4 @@ if (typeof exports === 'object' && exports === this) {
module.exports = tsort; module.exports = tsort;
if (process.argv[1] === __filename) tsortTest(); if (process.argv[1] === __filename) tsortTest();
} }
export default tsort