diff --git a/src/.eslintrc.cjs b/src/.eslintrc.cjs index 95c9efa07..9b0097b43 100644 --- a/src/.eslintrc.cjs +++ b/src/.eslintrc.cjs @@ -4,6 +4,9 @@ require('eslint-config-etherpad/patch/modern-module-resolution'); module.exports = { + "parserOptions": { + "sourceType": "module", + }, ignorePatterns: [ '/static/js/admin/jquery.autosize.js', '/static/js/admin/minify.json.js', diff --git a/src/node/db/API.js b/src/node/db/API.js index 3f92c47dd..e05af3449 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -19,19 +19,19 @@ * limitations under the License. */ -const Changeset = require('../../static/js/Changeset'); -const ChatMessage = require('../../static/js/ChatMessage'); -const CustomError = require('../utils/customError'); -const padManager = require('./PadManager'); -const padMessageHandler = require('../handler/PadMessageHandler'); -const readOnlyManager = require('./ReadOnlyManager'); -const groupManager = require('./GroupManager'); -const authorManager = require('./AuthorManager'); -const sessionManager = require('./SessionManager'); -const exportHtml = require('../utils/ExportHtml'); -const exportTxt = require('../utils/ExportTxt'); -const importHtml = require('../utils/ImportHtml'); -const cleanText = require('./Pad').cleanText; +import Changeset from '../../static/js/Changeset' +import ChatMessage from '../../static/js/ChatMessage' +import CustomError from '../utils/customError' +import padManager from './PadManager' +import padMessageHandler from '../handler/PadMessageHandler' +import readOnlyManager from './ReadOnlyManager' +import groupManager from './GroupManager' +import authorManager from './AuthorManager' +import sessionManager from './SessionManager' +import exportHtml from '../utils/ExportHtml' +import exportTxt from '../utils/ExportTxt' +import {setPadHTML} from '../utils/ImportHtml' +import {cleanText} from './Pad' const PadDiff = require('../utils/padDiff'); const { checkValidRev, isInt } = require('../utils/checkValidRev'); @@ -39,39 +39,39 @@ const { checkValidRev, isInt } = require('../utils/checkValidRev'); * GROUP FUNCTIONS **** ******************** */ -exports.listAllGroups = groupManager.listAllGroups; -exports.createGroup = groupManager.createGroup; -exports.createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor; -exports.deleteGroup = groupManager.deleteGroup; -exports.listPads = groupManager.listPads; -exports.createGroupPad = groupManager.createGroupPad; +export const listAllGroups = groupManager.listAllGroups; +export const createGroup = groupManager.createGroup; +export const createGroupIfNotExistsFor = groupManager.createGroupIfNotExistsFor; +export const deleteGroup = groupManager.deleteGroup; +export const listPads = groupManager.listPads; +export const createGroupPad = groupManager.createGroupPad; /* ******************** * PADLIST FUNCTION *** ******************** */ -exports.listAllPads = padManager.listAllPads; +export const listAllPads = padManager.listAllPads; /* ******************** * AUTHOR FUNCTIONS *** ******************** */ -exports.createAuthor = authorManager.createAuthor; -exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor; -exports.getAuthorName = authorManager.getAuthorName; -exports.listPadsOfAuthor = authorManager.listPadsOfAuthor; -exports.padUsers = padMessageHandler.padUsers; -exports.padUsersCount = padMessageHandler.padUsersCount; +export const createAuthor = authorManager.createAuthor; +export const createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor; +export const getAuthorName = authorManager.getAuthorName; +export const listPadsOfAuthor = authorManager.listPadsOfAuthor; +export const padUsers = padMessageHandler.padUsers; +export const padUsersCount = padMessageHandler.padUsersCount; /* ******************** * SESSION FUNCTIONS ** ******************** */ -exports.createSession = sessionManager.createSession; -exports.deleteSession = sessionManager.deleteSession; -exports.getSessionInfo = sessionManager.getSessionInfo; -exports.listSessionsOfGroup = sessionManager.listSessionsOfGroup; -exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor; +export const createSession = sessionManager.createSession; +export const deleteSession = sessionManager.deleteSession; +export const getSessionInfo = sessionManager.getSessionInfo; +export const listSessionsOfGroup = sessionManager.listSessionsOfGroup; +export const listSessionsOfAuthor = sessionManager.listSessionsOfAuthor; /* *********************** * PAD CONTENT FUNCTIONS * @@ -277,7 +277,7 @@ exports.setHTML = async (padID, html, authorId = '') => { // add a new changeset with the new html to the pad try { - await importHtml.setPadHTML(pad, cleanText(html), authorId); + await setPadHTML(pad, cleanText(html), authorId); } catch (e) { throw new CustomError('HTML is malformed', 'apierror'); } diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index 7049be5db..a2b823136 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -1,4 +1,3 @@ -'use strict'; /** * The AuthorManager controlls all information about the Pad authors */ @@ -19,12 +18,15 @@ * limitations under the License. */ -const db = require('./DB'); -const CustomError = require('../utils/customError'); -const hooks = require('../../static/js/pluginfw/hooks.js'); -const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils'); +import db from './DB.js'; -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', '#fff1c7', '#e3ffc7', @@ -94,23 +96,23 @@ exports.getColorPalette = () => [ /** * Checks if the author exists */ -exports.doesAuthorExist = async (authorID) => { +export const doesAuthorExist = async (authorID) => { const author = await db.get(`globalAuthor:${authorID}`); return author != null; }; /* 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); // return only the sub value authorID return author ? author.authorID : author; }; -exports.getAuthorId = async (token, user) => { +export const getAuthorId = async (token, user) => { const context = {dbKey: token, token, user}; let [authorId] = await hooks.aCallFirst('getAuthorId', context); if (!authorId) authorId = await getAuthor4Token(context.dbKey); @@ -123,10 +125,10 @@ exports.getAuthorId = async (token, user) => { * @deprecated Use `getAuthorId` instead. * @param {String} token The token */ -exports.getAuthor4Token = async (token) => { +export const getAuthor4Token = async (token) => { warnDeprecated( '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} name The name of the author (optional) */ -exports.createAuthorIfNotExistsFor = async (authorMapper, name) => { +export const createAuthorIfNotExistsFor = async (authorMapper, name) => { const author = await mapAuthorWithDBKey('mapper2author', authorMapper); if (name) { // set the name of this author - await exports.setAuthorName(author.authorID, name); + await setAuthorName(author.authorID, name); } return author; @@ -157,7 +159,7 @@ const mapAuthorWithDBKey = async (mapperkey, mapper) => { if (author == null) { // 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 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 * @param {String} name The name of the author */ -exports.createAuthor = async (name) => { +export const createAuthor = async (name) => { // create the new author name const author = `a.${randomString(16)}`; @@ -199,41 +201,41 @@ exports.createAuthor = async (name) => { * Returns the Author Obj 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 * @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 * @param {String} author The 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); /** * Returns the name 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 * @param {String} author The id 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); /** * Returns an array of all pads this author contributed to * @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: * (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 @@ -258,7 +260,7 @@ exports.listPadsOfAuthor = async (authorID) => { * @param {String} author The id of the author * @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 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} 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}`); if (author == null) return; diff --git a/src/node/db/DB.js b/src/node/db/DB.js index 02e83f85d..720d618df 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.js @@ -1,5 +1,3 @@ -'use strict'; - /** * The DB Module provides a database initialized with the settings * provided by the settings module @@ -21,40 +19,45 @@ * limitations under the License. */ -const ueberDB = require('ueberdb2'); -const settings = require('../utils/Settings'); -const log4js = require('log4js'); -const stats = require('../stats'); +import ueberDB from 'ueberdb2'; + +import {dbSettings, dbType} from '../utils/Settings.js'; + +import log4js from 'log4js'; + +import stats from '../stats.js'; const logger = log4js.getLogger('ueberDB'); /** * 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 */ -exports.init = async () => { - exports.db = new ueberDB.Database(settings.dbType, settings.dbSettings, null, logger); - await exports.db.init(); - if (exports.db.metrics != null) { - for (const [metric, value] of Object.entries(exports.db.metrics)) { +export const init = async () => { + db = new ueberDB.Database(dbType, dbSettings, null, logger); + await db.init(); + if (db.metrics != null) { + for (const [metric, value] of Object.entries(db.metrics)) { 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']) { - const f = exports.db[fn]; - exports[fn] = async (...args) => await f.call(exports.db, ...args); - Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f)); - Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f)); + const f = db[fn]; + global[fn] = async (...args) => await f.call(db, ...args); + Object.setPrototypeOf(global[fn], Object.getPrototypeOf(f)); + Object.defineProperties(global[fn], Object.getOwnPropertyDescriptors(f)); } }; -exports.shutdown = async (hookName, context) => { - if (exports.db != null) await exports.db.close(); - exports.db = null; +export const shutdown = async (hookName, context) => { + if (db != null) await db.close(); + db = null; logger.log('Database closed'); }; + +export default db; diff --git a/src/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js index 33ce2930a..762d2ba35 100644 --- a/src/node/db/ReadOnlyManager.js +++ b/src/node/db/ReadOnlyManager.js @@ -1,4 +1,3 @@ -'use strict'; /** * The ReadOnlyManager manages the database and rendering releated to read only pads */ @@ -20,21 +19,21 @@ */ -const db = require('./DB'); -const randomString = require('../utils/randomstring'); +import {db} from './DB.js'; +import randomString from '../utils/randomstring.js'; /** * checks if the id pattern matches a read-only pad 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 * @param {String} padId the id of the pad */ -exports.getReadOnlyId = async (padId) => { +export const getReadOnlyId = async (padId) => { // check if there is a pad2readonly entry let readOnlyId = await db.get(`pad2readonly:${padId}`); @@ -54,18 +53,18 @@ exports.getReadOnlyId = async (padId) => { * returns the padId for a 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 * @param {String} padIdOrReadonlyPadId read only id or real pad id */ -exports.getIds = async (id) => { - const readonly = exports.isReadOnlyId(id); +export const getIds = async (id) => { + const readonly = isReadOnlyId(id); // Might be null, if this is an unknown read-only id - const readOnlyPadId = readonly ? id : await exports.getReadOnlyId(id); - const padId = readonly ? await exports.getPadId(id) : id; + const readOnlyPadId = readonly ? id : await getReadOnlyId(id); + const padId = readonly ? await getPadId(id) : id; return {readOnlyPadId, padId, readonly}; }; diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js index 40e5e90d0..521f5d761 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.js @@ -1,13 +1,14 @@ -'use strict'; +import DB from './DB.js'; -const DB = require('./DB'); -const Store = require('express-session').Store; -const log4js = require('log4js'); -const util = require('util'); +import {Store} from 'express-session'; + +import log4js from 'log4js'; + +import util from 'util'; 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 * 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}`]); } -module.exports = SessionStore; diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js index 98be763c2..81423a382 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.js @@ -1,19 +1,34 @@ -'use strict'; +import _ from 'underscore'; -const _ = require('underscore'); -const SecretRotator = require('../security/SecretRotator'); -const cookieParser = require('cookie-parser'); -const events = require('events'); -const express = require('express'); -const expressSession = require('express-session'); -const fs = require('fs'); -const hooks = require('../../static/js/pluginfw/hooks'); -const log4js = require('log4js'); -const SessionStore = require('../db/SessionStore'); -const settings = require('../utils/Settings'); -const stats = require('../stats'); -const util = require('util'); -const webaccess = require('./express/webaccess'); +import * as SecretRotator from '../security/SecretRotator.js'; + +import cookieParser from 'cookie-parser'; + +import events from 'events'; + +import express from 'express'; + +import expressSession from 'express-session'; + +import fs from 'fs'; + +import * as hooks from '../../static/js/pluginfw/hooks.js'; + +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; const logger = log4js.getLogger('http'); @@ -23,14 +38,14 @@ const sockets = new Set(); const socketsEvents = new events.EventEmitter(); const startTime = stats.settableGauge('httpStartTime'); -exports.server = null; - +export let server = null; +export let sessionMiddleware; const closeServer = async () => { - if (exports.server != null) { + if (server != null) { logger.info('Closing HTTP server...'); // Call exports.server.close() to reject new connections but don't await just yet because the // Promise won't resolve until all preexisting connections are closed. - const p = util.promisify(exports.server.close.bind(exports.server))(); + const p = util.promisify(server.close.bind(server))(); await hooks.aCallAll('expressCloseServer'); // Give existing connections some time to close on their own before forcibly terminating. The // time should be long enough to avoid interrupting most preexisting transmissions but short @@ -49,7 +64,7 @@ const closeServer = async () => { } await p; clearTimeout(timeout); - exports.server = null; + server = null; startTime.setValue(0); logger.info('HTTP server closed'); } @@ -59,14 +74,14 @@ const closeServer = async () => { secretRotator = null; }; -exports.createServer = async () => { +export const createServer = async () => { console.log('Report bugs at https://github.com/ether/etherpad-lite/issues'); serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`; console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); - await exports.restartServer(); + await restartServer(); if (settings.ip === '') { // using Unix socket for connectivity @@ -91,7 +106,7 @@ exports.createServer = async () => { } }; -exports.restartServer = async () => { +export const restartServer = async () => { await closeServer(); const app = express(); // New syntax for express v3 @@ -113,12 +128,9 @@ exports.restartServer = async () => { options.ca.push(fs.readFileSync(caFileName)); } } - - const https = require('https'); - exports.server = https.createServer(options, app); + server = https.createServer(options, app); } else { - const http = require('http'); - exports.server = http.createServer(app); + server = http.createServer(app); } app.use((req, res, next) => { @@ -191,7 +203,7 @@ exports.restartServer = async () => { app.use(cookieParser(secret, {})); sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval); - exports.sessionMiddleware = expressSession({ + sessionMiddleware = expressSession({ propagateTouch: true, rolling: true, secret, @@ -229,15 +241,15 @@ exports.restartServer = async () => { // middleware. This allows plugins to avoid creating an express-session record in the database // when it is not needed (e.g., public static content). await hooks.aCallAll('expressPreSession', {app}); - app.use(exports.sessionMiddleware); + app.use(sessionMiddleware); app.use(webaccess.checkAccess); await Promise.all([ 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); socketsEvents.emit('updated'); socket.on('close', () => { @@ -245,11 +257,11 @@ exports.restartServer = async () => { socketsEvents.emit('updated'); }); }); - await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip); + await util.promisify(server.listen).bind(server)(settings.port, settings.ip); startTime.setValue(Date.now()); logger.info('HTTP server listening for connections'); }; -exports.shutdown = async (hookName, context) => { +export const shutdown = async (hookName, context) => { await closeServer(); }; diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index e0a5bd084..b62805af1 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -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 settings = require('../../utils/Settings'); -const hooks = require('../../../static/js/pluginfw/hooks'); -const readOnlyManager = require('../../db/ReadOnlyManager'); - -hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead'; +deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead'; // Promisified wrapper around hooks.aCallFirst. const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, reject) => { hooks.aCallFirst(hookName, context, (err, r) => err != null ? reject(err) : resolve(r), pred); }); + const aCallFirst0 = async (hookName, context, pred = null) => (await aCallFirst(hookName, context, pred))[0]; -exports.normalizeAuthzLevel = (level) => { +export const normalizeAuthzLevel = (level) => { if (!level) return false; switch (level) { case true: @@ -32,20 +36,20 @@ exports.normalizeAuthzLevel = (level) => { return false; }; -exports.userCanModify = (padId, req) => { +export const userCanModify = (padId, req) => { if (readOnlyManager.isReadOnlyId(padId)) return false; if (!settings.requireAuthentication) return true; const {session: {user} = {}} = req; if (!user || user.readOnly) return false; assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization. - const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]); + const level = normalizeAuthzLevel(user.padAuthorizations[padId]); return level && level !== 'readOnly'; }; // Exported so that tests can set this to 0 to avoid unnecessary test slowness. -exports.authnFailureDelayMs = 1000; +export const authnFailureDelayMs = 1000; -const checkAccess = async (req, res, next) => { +const checkAccessInternal = async (req, res, next) => { 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). const authorize = async () => { const grant = async (level) => { - level = exports.normalizeAuthzLevel(level); + level = normalizeAuthzLevel(level); if (!level) return false; const user = req.session.user; if (user == null) return true; // This will happen if authentication is not required. @@ -131,7 +135,8 @@ const checkAccess = async (req, res, next) => { // page). // /////////////////////////////////////////////////////////////////////////////////////////////// - if (settings.users == null) settings.users = {}; + // FIXME Not necessary + // if (settings.users == null) settings.users = {}; 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 // to authn plugins. @@ -159,7 +164,7 @@ const checkAccess = async (req, res, next) => { // No plugin handled the authentication failure. Fall back to basic authentication. res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); // Delay the error response for 1s to slow down brute force attacks. - await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs)); + await new Promise((resolve) => setTimeout(resolve, authnFailureDelayMs)); res.status(401).send('Authentication Required'); return; } @@ -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-session middleware. */ -exports.checkAccess = (req, res, next) => { - checkAccess(req, res, next).catch((err) => next(err || new Error(err))); +export const checkAccess = (req, res, next) => { + checkAccessInternal(req, res, next).catch((err) => next(err || new Error(err))); }; diff --git a/src/node/security/SecretRotator.js b/src/node/security/SecretRotator.js index b02a195cd..28d62f44a 100644 --- a/src/node/security/SecretRotator.js +++ b/src/node/security/SecretRotator.js @@ -1,9 +1,10 @@ -'use strict'; +import {Buffer} from 'buffer'; -const {Buffer} = require('buffer'); -const crypto = require('./crypto'); -const db = require('../db/DB'); -const log4js = require('log4js'); +import {hkdf,randomBytes} from './crypto.js'; + +import * as db from '../db/DB.js'; + +import log4js from 'log4js'; class Kdf { async generateParams() { throw new Error('not implemented'); } @@ -23,15 +24,15 @@ class Hkdf extends Kdf { async generateParams() { const [secret, salt] = (await Promise.all([ - crypto.randomBytes(this._keyLen), - crypto.randomBytes(this._keyLen), + randomBytes(this._keyLen), + randomBytes(this._keyLen), ])).map((b) => b.toString('hex')); return {digest: this._digest, keyLen: this._keyLen, salt, secret}; } async derive(p, info) { 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 * 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 {number} interval - How often to rotate in a new secret. @@ -97,7 +98,7 @@ class SecretRotator { async _publish(params, id = null) { // Params are published to the db with a randomly generated key to avoid race conditions with // 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); return id; } @@ -247,5 +248,3 @@ class SecretRotator { this._t.setTimeout(async () => await this._update(), next - this._t.now()); } } - -module.exports = SecretRotator; diff --git a/src/node/security/crypto.js b/src/node/security/crypto.js index ebe918509..75e3c37d1 100644 --- a/src/node/security/crypto.js +++ b/src/node/security/crypto.js @@ -1,15 +1,13 @@ -'use strict'; - -const crypto = require('crypto'); -const util = require('util'); +import crypto from 'crypto'; +import util from 'util'; /** * 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 */ -exports.randomBytes = util.promisify(crypto.randomBytes); \ No newline at end of file +export const randomBytes = util.promisify(crypto.randomBytes); diff --git a/src/node/server.js b/src/node/server.js index ff1920af8..7168cadcc 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -1,6 +1,5 @@ #!/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. @@ -24,35 +23,47 @@ * limitations under the License. */ -const log4js = require('log4js'); + +import log4js from 'log4js'; + log4js.replaceConsole(); +import * as settings from './utils/Settings.js'; +import {dumpOnUncleanExit} from './utils/Settings.js'; -const settings = require('./utils/Settings'); -let wtfnode; -if (settings.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 = require('wtfnode'); -} +import wtfnode0 from 'wtfnode'; + +import {check} from './utils/UpdateCheck.js'; + +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 * any modules that require newer versions of NodeJS */ -const NodeVersion = require('./utils/NodeVersion'); -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'); +import {enforceMinNodeVersion, checkDeprecationStatus} from './utils/NodeVersion.js'; +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 State = { @@ -76,14 +87,14 @@ const removeSignalListener = (signal, listener) => { }; let startDoneGate; -exports.start = async () => { +export const start = async () => { switch (state) { case State.INITIAL: break; case State.STARTING: await startDoneGate; // Retry. Don't fall through because it might have transitioned to STATE_TRANSITION_FAILED. - return await exports.start(); + return await start(); case State.RUNNING: return express.server; case State.STOPPING: @@ -100,7 +111,7 @@ exports.start = async () => { state = State.STARTING; try { // Check if Etherpad version is up-to-date - UpdateCheck.check(); + check(); stats.gauge('memoryUsage', () => process.memoryUsage().rss); stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); @@ -109,7 +120,7 @@ exports.start = async () => { logger.debug(`uncaught exception: ${err.stack || err}`); // eslint-disable-next-line promise/no-promise-in-callback - exports.exit(err) + exit(err) .catch((err) => { logger.error('Error in process exit', JSON.stringify(err)); // eslint-disable-next-line n/no-process-exit @@ -131,7 +142,7 @@ exports.start = async () => { for (const listener of process.listeners(signal)) { removeSignalListener(signal, listener); } - process.on(signal, exports.exit); + process.on(signal, exit); // Prevent signal listeners from being added in the future. process.on('newListener', (event, listener) => { if (event !== signal) return; @@ -141,7 +152,7 @@ exports.start = async () => { await db.init(); await plugins.update(); - const installedPlugins = Object.values(pluginDefs.plugins) + const installedPlugins = Object.values(pluginDefs) .filter((plugin) => plugin.package.name !== 'ep_etherpad-lite') .map((plugin) => `${plugin.package.name}@${plugin.package.version}`) .join(', '); @@ -154,7 +165,7 @@ exports.start = async () => { logger.error('Error occurred while starting Etherpad'); state = State.STATE_TRANSITION_FAILED; startDoneGate.resolve(); - return await exports.exit(err); + return await exit(err); } logger.info('Etherpad is running'); @@ -166,12 +177,12 @@ exports.start = async () => { }; const stopDoneGate = new Gate(); -exports.stop = async () => { +export const stop = async () => { switch (state) { case State.STARTING: - await exports.start(); + await start(); // 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: break; case State.STOPPING: @@ -201,7 +212,7 @@ exports.stop = async () => { logger.error('Error occurred while stopping Etherpad'); state = State.STATE_TRANSITION_FAILED; stopDoneGate.resolve(); - return await exports.exit(err); + return await exit(err); } logger.info('Etherpad stopped'); state = State.STOPPED; @@ -210,7 +221,7 @@ exports.stop = async () => { let exitGate; let exitCalled = false; -exports.exit = async (err = null) => { +export const exit = async (err = null) => { /* eslint-disable no-process-exit */ if (err === 'SIGTERM') { // Termination from SIGTERM is not treated as an abnormal termination. @@ -222,6 +233,7 @@ exports.exit = async (err = null) => { process.exitCode = 1; if (exitCalled) { logger.error('Error occurred while waiting to exit. Forcing an immediate unclean exit...'); + // eslint-disable-next-line n/no-process-exit process.exit(1); } } @@ -231,11 +243,11 @@ exports.exit = async (err = null) => { case State.STARTING: case State.RUNNING: 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 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.) - return await exports.exit(); + return await exit(); case State.INITIAL: case State.STOPPED: case State.STATE_TRANSITION_FAILED: @@ -267,6 +279,7 @@ exports.exit = async (err = null) => { } logger.error('Forcing an unclean exit...'); + // eslint-disable-next-line n/no-process-exit process.exit(1); }, 5000).unref(); @@ -275,4 +288,4 @@ exports.exit = async (err = null) => { /* eslint-enable no-process-exit */ }; -if (require.main === module) exports.start(); +start(); diff --git a/src/node/stats.js b/src/node/stats.js index cecaca20d..48b1a63ba 100644 --- a/src/node/stats.js +++ b/src/node/stats.js @@ -1,9 +1,9 @@ -'use strict'; +import measured from 'measured-core'; -const measured = require('measured-core'); +export default measured.createCollection(); -module.exports = measured.createCollection(); - -module.exports.shutdown = async (hookName, context) => { - module.exports.end(); +export const shutdown = async (hookName, context) => { + // FIXME Is this correcT? + // eslint-disable-next-line n/no-process-exit + process.exit(0); }; diff --git a/src/node/utils/AbsolutePaths.js b/src/node/utils/AbsolutePaths.js index 73a96bb67..4d3338449 100644 --- a/src/node/utils/AbsolutePaths.js +++ b/src/node/utils/AbsolutePaths.js @@ -1,4 +1,3 @@ -'use strict'; /** * Library for deterministic relative filename expansion for Etherpad. */ @@ -19,12 +18,20 @@ * limitations under the License. */ -const log4js = require('log4js'); -const path = require('path'); -const _ = require('underscore'); +import log4js from 'log4js'; + +import path from 'path'; + +import _ from 'underscore'; + +import findRoot from 'find-root'; +import {fileURLToPath} from 'url'; const absPathLogger = log4js.getLogger('AbsolutePaths'); +const __filename = fileURLToPath(import.meta.url); + +const __dirname = path.dirname(__filename); /* * findEtherpadRoot() computes its value only on first invocation. * Subsequent invocations are served from this variable. @@ -49,6 +56,7 @@ const popIfEndsWith = (stringArray, lastDesiredElements) => { return false; } + // eslint-disable-next-line you-dont-need-lodash-underscore/last const lastElementsFound = _.last(stringArray, lastDesiredElements.length); if (_.isEqual(lastElementsFound, lastDesiredElements)) { @@ -75,12 +83,10 @@ const popIfEndsWith = (stringArray, lastDesiredElements) => { * @return {string} The identified absolute base path. If such path cannot be * identified, prints a log and exits the application. */ -exports.findEtherpadRoot = () => { +export const findEtherpadRoot = () => { if (etherpadRoot != null) { return etherpadRoot; } - - const findRoot = require('find-root'); const foundRoot = findRoot(__dirname); const splitFoundRoot = foundRoot.split(path.sep); @@ -106,6 +112,7 @@ exports.findEtherpadRoot = () => { if (maybeEtherpadRoot === false) { absPathLogger.error('Could not identity Etherpad base path in this ' + `${process.platform} installation in "${foundRoot}"`); + // eslint-disable-next-line n/no-process-exit process.exit(1); } @@ -118,6 +125,7 @@ exports.findEtherpadRoot = () => { absPathLogger.error( `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); }; @@ -131,12 +139,12 @@ exports.findEtherpadRoot = () => { * it is returned unchanged. Otherwise it is interpreted * relative to exports.root. */ -exports.makeAbsolute = (somePath) => { +export const makeAbsolute = (somePath) => { if (path.isAbsolute(somePath)) { return somePath; } - const rewrittenPath = path.join(exports.findEtherpadRoot(), somePath); + const rewrittenPath = path.join(findEtherpadRoot(), somePath); absPathLogger.debug(`Relative path "${somePath}" can be rewritten to "${rewrittenPath}"`); return rewrittenPath; @@ -150,7 +158,7 @@ exports.makeAbsolute = (somePath) => { * a subdirectory of the base one * @return {boolean} */ -exports.isSubdir = (parent, arbitraryDir) => { +export const isSubdir = (parent, arbitraryDir) => { // modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825 const relative = path.relative(parent, arbitraryDir); const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); diff --git a/src/node/utils/Cli.js b/src/node/utils/Cli.js index a5cdee83a..eded6ac70 100644 --- a/src/node/utils/Cli.js +++ b/src/node/utils/Cli.js @@ -21,33 +21,33 @@ */ // 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; // Loop through args -for (let i = 0; i < argv.length; i++) { - arg = argv[i]; +for (let i = 0; i < argvProcess.length; i++) { + arg = argvProcess[i]; // Override location of settings.json file if (prevArg === '--settings' || prevArg === '-s') { - exports.argv.settings = arg; + argv.settings = arg; } // Override location of credentials.json file if (prevArg === '--credentials') { - exports.argv.credentials = arg; + argv.credentials = arg; } // Override location of settings.json file if (prevArg === '--sessionkey') { - exports.argv.sessionkey = arg; + argv.sessionkey = arg; } // Override location of settings.json file if (prevArg === '--apikey') { - exports.argv.apikey = arg; + argv.apikey = arg; } prevArg = arg; diff --git a/src/node/utils/NodeVersion.js b/src/node/utils/NodeVersion.js index ca5412f23..0db8fb86f 100644 --- a/src/node/utils/NodeVersion.js +++ b/src/node/utils/NodeVersion.js @@ -19,14 +19,14 @@ * limitations under the License. */ -const semver = require('semver'); +import semver from 'semver'; /** * Quits if Etherpad is not running on a given minimum Node version * * @param {String} minNodeVersion Minimum required Node version */ -exports.enforceMinNodeVersion = (minNodeVersion) => { +export const enforceMinNodeVersion = (minNodeVersion) => { const currentNodeVersion = process.version; // 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)) { console.error(`Running Etherpad on Node ${currentNodeVersion} is not supported. ` + `Please upgrade at least to Node ${minNodeVersion}`); + // eslint-disable-next-line n/no-process-exit process.exit(1); } @@ -49,7 +50,7 @@ exports.enforceMinNodeVersion = (minNodeVersion) => { * @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated * Node releases */ -exports.checkDeprecationStatus = (lowestNonDeprecatedNodeVersion, epRemovalVersion) => { +export const checkDeprecationStatus = (lowestNonDeprecatedNodeVersion, epRemovalVersion) => { const currentNodeVersion = process.version; if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) { diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 512bc6f4c..c753cb127 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -1,4 +1,3 @@ -'use strict'; /** * The Settings module reads the settings out of settings.json and provides * this information to the other modules @@ -27,19 +26,32 @@ * limitations under the License. */ -const absolutePaths = require('./AbsolutePaths'); -const deepEqual = require('fast-deep-equal/es6'); -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const argv = require('./Cli').argv; -const jsonminify = require('jsonminify'); -const log4js = require('log4js'); -const randomString = require('./randomstring'); +import {findEtherpadRoot, isSubdir, makeAbsolute} from './AbsolutePaths.js'; + +import deepEqual from 'fast-deep-equal/es6/index.js'; + +// eslint-disable-next-line n/no-deprecated-api +import {readFileSync, lstatSync, writeFileSync, exists, existsSync} from 'fs'; + +import os from 'os'; + +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 ' + 'suppressErrorsInPadText to true in your settings.json\n'; -const _ = require('underscore'); - const logger = log4js.getLogger('settings'); // Exported values that settings.json and credentials.json cannot override. @@ -68,16 +80,17 @@ const initLogging = (logLevel, config) => { initLogging(defaultLogLevel, defaultLogConfig()); /* 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 ' + - `Etherpad base dir: ${exports.root}`); -exports.settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json'); -exports.credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'); + `Etherpad base dir: ${root}`); +export const settingsFilename = makeAbsolute(argv.settings || 'settings.json'); +export const credentialsFilename = makeAbsolute(argv.credentials || + 'credentials.json'); /** * 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 @@ -85,7 +98,7 @@ exports.title = 'Etherpad'; * is used. If this is a relative path it is interpreted as relative to the * Etherpad root directory. */ -exports.favicon = null; +export const favicon = null; /* * Skin name. @@ -93,37 +106,37 @@ exports.favicon = null; * Initialized to null, so we can spot an old configuration file and invite the * user to update it before falling back to the default. */ -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 */ -exports.ip = '0.0.0.0'; +export const ip = '0.0.0.0'; /** * 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 */ -exports.suppressErrorsInPadText = false; +export const suppressErrorsInPadText = false; /** * 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. */ -exports.ssl = false; +export const ssl = false; /** * 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). * @@ -138,16 +151,16 @@ exports.socketIo = { /* * 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 */ -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 */ -exports.defaultPadText = [ +export let defaultPadText = [ 'Welcome to Etherpad!', '', '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 */ -exports.padOptions = { +export const padOptions = { noColors: false, showControls: true, showChat: true, @@ -176,7 +189,7 @@ exports.padOptions = { /** * Whether certain shortcut keys are enabled for a user in the pad */ -exports.padShortcutEnabled = { +export const padShortcutEnabled = { altF9: true, altC: true, delete: true, @@ -204,7 +217,7 @@ exports.padShortcutEnabled = { /** * The toolbar buttons and order. */ -exports.toolbar = { +export const toolbar = { left: [ ['bold', 'italic', 'underline', 'strikethrough'], ['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 */ -exports.requireSession = false; +export const requireSession = false; /** * 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). */ -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 */ -exports.minify = true; +export const minify = true; /** * The path of the abiword executable */ -exports.abiword = null; +export let abiword = null; /** * The path of the libreoffice executable */ -exports.soffice = null; +export let soffice = null; /** * The path of the tidy executable */ -exports.tidyHtml = null; +export const tidyHtml = null; /** * Should we support none natively supported file types on import? */ -exports.allowUnknownFileEnds = true; +export const allowUnknownFileEnds = true; /** * The log level of log4js */ -exports.loglevel = defaultLogLevel; +export const loglevel = defaultLogLevel; /** * Disable IP logging */ -exports.disableIPlogging = false; +export const disableIPlogging = false; /** * Number of seconds to automatically reconnect pad */ -exports.automaticReconnectionTimeout = 0; +export const automaticReconnectionTimeout = 0; /** * Disable Load Testing */ -exports.loadTest = false; +export const loadTest = false; /** * Disable dump of objects preventing a clean exit */ -exports.dumpOnUncleanExit = false; +export const dumpOnUncleanExit = false; /** * Enable indentation on new lines */ -exports.indentationOnNewLine = true; +export const indentationOnNewLine = true; /* * log4js appender configuration */ -exports.logconfig = defaultLogConfig(); +export const logconfig = defaultLogConfig(); /* * Deprecated cookie signing key. */ -exports.sessionKey = null; +export let sessionKey = null; /* * 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. */ -exports.cookie = { +export const cookie = { keyRotationInterval: 1 * 24 * 60 * 60 * 1000, /* * Value of the SameSite cookie property. "Lax" is recommended unless @@ -332,20 +345,20 @@ exports.cookie = { * authorization. Note: /admin always requires authentication, and * either authorization by a module, or a user with is_admin set */ -exports.requireAuthentication = false; -exports.requireAuthorization = false; -exports.users = {}; +export const requireAuthentication = false; +export const requireAuthorization = false; +export let users = {}; /* * 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 * height needed to make this line visible. */ -exports.scrollWhenFocusLineIsOutOfViewport = { +export const scrollWhenFocusLineIsOutOfViewport = { /* * Percentage of viewport height to be additionally scrolled. */ @@ -378,12 +391,12 @@ exports.scrollWhenFocusLineIsOutOfViewport = { * * Do not enable on production machines. */ -exports.exposeVersion = false; +export const exposeVersion = false; /* * 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 @@ -394,7 +407,7 @@ exports.customLocaleStrings = {}; * * See https://github.com/nfriedly/express-rate-limit for more options */ -exports.importExportRateLimiting = { +export const importExportRateLimiting = { // duration of the rate limit window (milliseconds) 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 */ -exports.commitRateLimiting = { +export const commitRateLimiting = { // duration of the rate limit window (seconds) duration: 1, @@ -424,39 +437,39 @@ exports.commitRateLimiting = { * * 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 */ -exports.enableAdminUITests = false; +export const enableAdminUITests = false; /* * Enable auto conversion of pad Ids to lowercase. * e.g. /p/EtHeRpAd to /p/etherpad */ -exports.lowerCasePadIds = false; +export const lowerCasePadIds = false; // checks if abiword is avaiable -exports.abiwordAvailable = () => { - if (exports.abiword != null) { +export const abiwordAvailable = () => { + if (abiword != null) { return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; } else { return 'no'; } }; -exports.sofficeAvailable = () => { - if (exports.soffice != null) { +export const sofficeAvailable = () => { + if (soffice != null) { return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; } else { return 'no'; } }; -exports.exportAvailable = () => { - const abiword = exports.abiwordAvailable(); - const soffice = exports.sofficeAvailable(); +export const exportAvailable = () => { + const abiword = abiwordAvailable(); + const soffice = sofficeAvailable(); if (abiword === 'no' && soffice === 'no') { return 'no'; @@ -469,20 +482,20 @@ exports.exportAvailable = () => { }; // Provide git version if available -exports.getGitCommit = () => { +export const getGitCommit = () => { let version = ''; try { - let rootPath = exports.root; - if (fs.lstatSync(`${rootPath}/.git`).isFile()) { - rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8'); + let rootPath = root; + if (lstatSync(`${rootPath}/.git`).isFile()) { + rootPath = readFileSync(`${rootPath}/.git`, 'utf8'); rootPath = rootPath.split(' ').pop().trim(); } else { rootPath += '/.git'; } - const ref = fs.readFileSync(`${rootPath}/HEAD`, 'utf-8'); + const ref = readFileSync(`${rootPath}/HEAD`, 'utf-8'); if (ref.startsWith('ref: ')) { const refPath = `${rootPath}/${ref.substring(5, ref.indexOf('\n'))}`; - version = fs.readFileSync(refPath, 'utf-8'); + version = readFileSync(refPath, 'utf-8'); } else { version = ref; } @@ -494,7 +507,7 @@ exports.getGitCommit = () => { }; // 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 @@ -517,11 +530,13 @@ const storeSettings = (settingsObj) => { // we know this setting, so we overwrite it // 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])) { - exports[i] = _.defaults(settingsObj[i], exports[i]); + variable = _.defaults(settingsObj[i], variable); } else { - exports[i] = settingsObj[i]; + variable = settingsObj[i]; } } else { // this setting is unknown, output a warning and throw it away @@ -702,7 +717,7 @@ const parseSettings = (settingsFilename, isSettings) => { try { // read the settings file - settingsStr = fs.readFileSync(settingsFilename).toString(); + settingsStr = readFileSync(settingsFilename).toString(); } catch (e) { notFoundFunction(`No ${settingsType} file found in ${settingsFilename}. ${notFoundMessage}`); @@ -717,107 +732,106 @@ const parseSettings = (settingsFilename, isSettings) => { logger.info(`${settingsType} loaded from: ${settingsFilename}`); - const replacedSettings = lookupEnvironmentVariables(settings); - - return replacedSettings; + return lookupEnvironmentVariables(settings); } catch (e) { logger.error(`There was an error processing your ${settingsType} ` + `file from ${settingsFilename}: ${e.message}`); + // eslint-disable-next-line n/no-process-exit process.exit(1); } }; -exports.reloadSettings = () => { - const settings = parseSettings(exports.settingsFilename, true); - const credentials = parseSettings(exports.credentialsFilename, false); +export const reloadSettings = () => { + const settings = parseSettings(settingsFilename, true); + const credentials = parseSettings(credentialsFilename, false); storeSettings(settings); storeSettings(credentials); - initLogging(exports.loglevel, exports.logconfig); + initLogging(loglevel, logconfig); - if (!exports.skinName) { + if (!skinName) { logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' + 'update your settings.json. Falling back to the default "colibris".'); - exports.skinName = 'colibris'; + skinName = 'colibris'; } // checks if skinName has an acceptable value, otherwise falls back to "colibris" - if (exports.skinName) { - const skinBasePath = path.join(exports.root, 'src', 'static', 'skins'); - const countPieces = exports.skinName.split(path.sep).length; + if (skinName) { + const skinBasePath = path.join(root, 'src', 'static', 'skins'); + const countPieces = skinName.split(path.sep).length; if (countPieces !== 1) { logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` + - `not valid: "${exports.skinName}". Falling back to the default "colibris".`); + `not valid: "${skinName}". Falling back to the default "colibris".`); - exports.skinName = 'colibris'; + skinName = 'colibris'; } // informative variable, just for the log messages - let skinPath = path.join(skinBasePath, exports.skinName); + let skinPath = path.join(skinBasePath, skinName); // what if someone sets skinName == ".." or "."? We catch him! - if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) { + if (isSubdir(skinBasePath, skinPath) === false) { logger.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` + 'Falling back to the default "colibris".'); - exports.skinName = 'colibris'; - skinPath = path.join(skinBasePath, exports.skinName); + skinName = 'colibris'; + skinPath = path.join(skinBasePath, skinName); } - if (fs.existsSync(skinPath) === false) { + if (existsSync(skinPath) === false) { logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); - exports.skinName = 'colibris'; - skinPath = path.join(skinBasePath, exports.skinName); + skinName = 'colibris'; + skinPath = path.join(skinBasePath, skinName); } - logger.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`); + logger.info(`Using skin "${skinName}" in dir: ${skinPath}`); } - if (exports.abiword) { + if (abiword) { // Check abiword actually exists - if (exports.abiword != null) { - fs.exists(exports.abiword, (exists) => { + if (abiword != null) { + exists(abiword, (exists) => { if (!exists) { const abiwordError = 'Abiword does not exist at this path, check your settings file.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`; + if (!suppressErrorsInPadText) { + defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`; } - logger.error(`${abiwordError} File location: ${exports.abiword}`); - exports.abiword = null; + logger.error(`${abiwordError} File location: ${abiword}`); + abiword = null; } }); } } - if (exports.soffice) { - fs.exists(exports.soffice, (exists) => { + if (soffice) { + exists(soffice, (exists) => { if (!exists) { const sofficeError = 'soffice (libreoffice) does not exist at this path, check your settings file.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`; + if (!suppressErrorsInPadText) { + defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`; } - logger.error(`${sofficeError} File location: ${exports.soffice}`); - exports.soffice = null; + logger.error(`${sofficeError} File location: ${soffice}`); + soffice = null; } }); } - const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); - if (!exports.sessionKey) { + const sessionkeyFilename = makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); + if (!sessionKey) { try { - exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); + sessionKey = readFileSync(sessionkeyFilename, 'utf8'); logger.info(`Session key loaded from: ${sessionkeyFilename}`); } catch (err) { /* ignored */ } - const keyRotationEnabled = exports.cookie.keyRotationInterval && exports.cookie.sessionLifetime; - if (!exports.sessionKey && !keyRotationEnabled) { + const keyRotationEnabled = cookie.keyRotationInterval && cookie.sessionLifetime; + if (!sessionKey && !keyRotationEnabled) { logger.info( `Session key file "${sessionkeyFilename}" not found. Creating with random contents.`); - exports.sessionKey = randomString(32); - fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8'); + sessionKey = randomString(32); + writeFileSync(sessionkeyFilename, sessionKey, 'utf8'); } } else { 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 ' + 'Interface then you can ignore this message.'); } - if (exports.sessionKey) { + if (sessionKey) { logger.warn(`The sessionKey setting and ${sessionkeyFilename} file are deprecated; ` + '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.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`; + if (!suppressErrorsInPadText) { + defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`; } - exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename); - logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`); + dbSettings.filename = makeAbsolute(dbSettings.filename); + logger.warn(`${dirtyWarning} File location: ${dbSettings.filename}`); } - if (exports.ip === '') { + if (ip === '') { // using Unix socket for connectivity logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' + '"port" parameter will be interpreted as the path to a Unix socket to bind at.'); @@ -857,13 +871,13 @@ exports.reloadSettings = () => { * ACHTUNG: this may prevent caching HTTP proxies to work * TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead */ - exports.randomVersionString = randomString(4); - logger.info(`Random string used for versioning assets: ${exports.randomVersionString}`); + const randomVersionString = randomString(4); + logger.info(`Random string used for versioning assets: ${randomVersionString}`); }; -exports.exportedForTestingOnly = { +export const exportedForTestingOnly = { parseSettings, }; // initially load settings -exports.reloadSettings(); +reloadSettings(); diff --git a/src/node/utils/UpdateCheck.js b/src/node/utils/UpdateCheck.js index 44248d29c..82ccd5ef6 100644 --- a/src/node/utils/UpdateCheck.js +++ b/src/node/utils/UpdateCheck.js @@ -1,45 +1,44 @@ -'use strict'; -const semver = require('semver'); -const settings = require('./Settings'); -const axios = require('axios'); -let infos; +import axios from 'axios'; -const loadEtherpadInformations = () => - axios.get('https://static.etherpad.org/info.json') - .then(async resp => { - try { - infos = await resp.data; - if (infos === undefined || infos === null) { - await Promise.reject("Could not retrieve current version") - return - } - return await Promise.resolve(infos); - } - catch (err) { - return await Promise.reject(err); - } - }) +import {getEpVersion} from './Settings.js'; + +import semver from 'semver'; -exports.getLatestVersion = () => { - exports.needsUpdate(); - return infos.latestVersion; +const loadEtherpadInformations = () => axios.get('https://static.etherpad.org/info.json') + .then(async (resp) => { + try { + const infos = await resp.data; + if (infos === undefined || infos == null) { + await Promise.reject(new Error('Could not retrieve current version')); + return; + } + return infos; + } catch (err) { + return err; + } + }); + + +export const getLatestVersion = () => { + const infos = needsUpdate(); + return infos; }; -exports.needsUpdate = async (cb) => { +export const needsUpdate = async (cb) => { await loadEtherpadInformations() .then((info) => { - if (semver.gt(info.latestVersion, settings.getEpVersion())) { - if (cb) return cb(true); - } - }).catch((err) => { - console.error(`Can not perform Etherpad update check: ${err}`); - if (cb) return cb(false); - }); + if (semver.gt(info.latestVersion, getEpVersion())) { + if (cb) return cb(true); + } + }).catch((err) => { + console.error(`Can not perform Etherpad update check: ${err}`); + if (cb) return cb(false); + }); }; -exports.check = () => { - exports.needsUpdate((needsUpdate) => { +export const check = () => { + needsUpdate((needsUpdate) => { if (needsUpdate) { console.warn(`Update available: Download the actual version ${infos.latestVersion}`); } diff --git a/src/node/utils/promises.js b/src/node/utils/promises.js index bc9f8c2dc..1c1b2cd83 100644 --- a/src/node/utils/promises.js +++ b/src/node/utils/promises.js @@ -1,4 +1,4 @@ -'use strict'; + /** * 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 // `promises` is empty. If `predicate` is nullish, the truthiness of the resolved value is used as // the predicate. -exports.firstSatisfies = (promises, predicate) => { +export const firstSatisfies = (promises, predicate) => { if (predicate == null) predicate = (x) => x; // Transform each original Promise into a Promise that never resolves if the original resolved @@ -42,7 +42,7 @@ exports.firstSatisfies = (promises, predicate) => { // `total` is greater than `concurrency`, then `concurrency` Promises will be created right away, // and each remaining Promise will be created once one of the earlier Promises resolves.) This async // function resolves once all `total` Promises have resolved. -exports.timesLimit = async (total, concurrency, promiseCreator) => { +export const timesLimit = async (total, concurrency, promiseCreator) => { if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive'); let next = 0; const addAnother = () => promiseCreator(next++).finally(() => { @@ -59,7 +59,7 @@ exports.timesLimit = async (total, concurrency, promiseCreator) => { * An ordinary Promise except the `resolve` and `reject` executor functions are exposed as * properties. */ -class Gate extends Promise { +export class Gate extends Promise { // Coax `.then()` into returning an ordinary Promise, not a Gate. See // https://stackoverflow.com/a/65669070 for the rationale. static get [Symbol.species]() { return Promise; } @@ -73,4 +73,3 @@ class Gate extends Promise { Object.assign(this, props); } } -exports.Gate = Gate; diff --git a/src/node/utils/randomstring.js b/src/node/utils/randomstring.js index 4ffd3e8ae..969b30e07 100644 --- a/src/node/utils/randomstring.js +++ b/src/node/utils/randomstring.js @@ -1,10 +1,9 @@ -'use strict'; /** * Generates a random String with the given length. Is needed to generate the * Author, Group, readonly, session Ids */ -const crypto = require('crypto'); +import crypto from 'crypto'; const randomString = (len) => crypto.randomBytes(len).toString('hex'); -module.exports = randomString; +export default randomString; diff --git a/src/node/utils/run_cmd.js b/src/node/utils/run_cmd.js index bf5515c84..05a057cd3 100644 --- a/src/node/utils/run_cmd.js +++ b/src/node/utils/run_cmd.js @@ -1,9 +1,10 @@ -'use strict'; +import spawn from 'cross-spawn'; -const spawn = require('cross-spawn'); -const log4js = require('log4js'); -const path = require('path'); -const settings = require('./Settings'); +import log4js from 'log4js'; + +import path from 'path'; + +import * as settings from './Settings.js'; const logger = log4js.getLogger('runCmd'); @@ -69,7 +70,7 @@ const logLines = (readable, logLineFn) => { * - `stderr`: Similar to `stdout` but for stderr. * - `child`: The ChildProcess object. */ -module.exports = exports = (args, opts = {}) => { +export default (args, opts = {}) => { logger.debug(`Executing command: ${args.join(' ')}`); opts = {cwd: settings.root, ...opts}; diff --git a/src/package.json b/src/package.json index e6436da9a..4d7a06d94 100644 --- a/src/package.json +++ b/src/package.json @@ -2,6 +2,7 @@ "name": "ep_etherpad-lite", "description": "A free and open source realtime collaborative editor", "homepage": "https://etherpad.org", + "type": "module", "keywords": [ "etherpad", "realtime", diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 89f4267bb..29a86ba2c 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -1,6 +1,6 @@ '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 // (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'); // hooks.deprecationNotices.fooBar = 'use the newSpiffy hook instead'; // -exports.deprecationNotices = {}; +export const deprecationNotices = {}; const deprecationWarned = {}; const checkDeprecation = (hook) => { - const notice = exports.deprecationNotices[hook.hook_name]; + const notice = deprecationNotices[hook.hook_name]; if (notice == null) return; if (deprecationWarned[hook.hook_fn_name]) return; 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. // 2. Convert each `undefined` entry into `[]`. // 3. Flatten one level. -exports.callAll = (hookName, context) => { +export const callAll = (hookName, context) => { if (context == null) context = {}; const hooks = pluginDefs.hooks[hookName] || []; return flatten1(hooks.map((hook) => normalizeValue(callHookFnSync(hook, context)))); @@ -342,7 +342,7 @@ const callHookFnAsync = async (hook, context) => { // 2. Convert each `undefined` entry into `[]`. // 3. Flatten one level. // 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 (context == null) context = {}; 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. // Only use this function if the hook functions must be called one at a time, otherwise use // `aCallAll()`. -exports.callAllSerial = async (hookName, context) => { +export const callAllSerial = async (hookName, context) => { if (context == null) context = {}; const hooks = pluginDefs.hooks[hookName] || []; const results = []; @@ -367,7 +367,7 @@ exports.callAllSerial = async (hookName, context) => { // DEPRECATED: Use `aCallFirst()` instead. // // Like `aCallFirst()`, but synchronous. Hook functions must provide their values synchronously. -exports.callFirst = (hookName, context) => { +export const callFirst = (hookName, context) => { if (context == null) context = {}; const predicate = (val) => val.length; 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 // predicate or empty if the predicate was never satisfied. If cb is non-nullish, resolves to the // 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) { - return await attachCallback(exports.aCallFirst(hookName, context, null, predicate), cb); + return await attachCallback(aCallFirst(hookName, context, null, predicate), cb); } if (context == null) context = {}; if (predicate == null) predicate = (val) => val.length; @@ -413,7 +413,7 @@ exports.aCallFirst = async (hookName, context, cb = null, predicate = null) => { return []; }; -exports.exportedForTestingOnly = { +export const exportedForTestingOnly = { callHookFnAsync, callHookFnSync, deprecationWarned, diff --git a/src/static/js/pluginfw/plugin_defs.js b/src/static/js/pluginfw/plugin_defs.js index f7d10879e..3ce7ed2f5 100644 --- a/src/static/js/pluginfw/plugin_defs.js +++ b/src/static/js/pluginfw/plugin_defs.js @@ -8,13 +8,13 @@ // * hook_fn: Plugin-supplied hook function. // * hook_fn_name: Name of the hook function, with the form :. // * part: The ep.json part object that declared the hook. See exports.plugins. -exports.hooks = {}; +export let hooks = {}; // Whether the plugins have been loaded. -exports.loaded = false; +export let loaded = false; // 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 // augmented with additional metadata: @@ -25,4 +25,11 @@ exports.parts = []; // - version // - path // - 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 diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index ec3cfaa92..11314aa5e 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -1,13 +1,13 @@ 'use strict'; -const fs = require('fs').promises; -const hooks = require('./hooks'); -const log4js = require('log4js'); -const path = require('path'); -const runCmd = require('../../../node/utils/run_cmd'); -const tsort = require('./tsort'); -const pluginUtils = require('./shared'); -const defs = require('./plugin_defs'); +import {promises} from 'fs' +import * as hooks from './hooks.js' +import log4js from 'log4js' +import path from 'path' +import runCmd from '../../../node/utils/run_cmd.js' +import tsort from './tsort.js' +import * as pluginUtils from './shared.js' +import {parts, plugins, loaded, setPlugins, setParts, setHooks, setLoaded} from './plugin_defs.js' 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(); - 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 [hookName, hookFnName] of Object.entries(part[hookSetName] || {})) { let hookEntry = hooks.get(hookName); @@ -72,39 +72,39 @@ exports.formatHooks = (hookSetName, html) => { 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'. // If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'. const functionName = (tmp.length > 1 ? tmp.pop() : null) || hookName; 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); return `${fileName}:${functionName}`; }; -exports.update = async () => { - const packages = await exports.getPackages(); - const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array. - const plugins = {}; +export const update = async () => { + const packagesNew = await getPackages(); + let partsNew = {}; // Key is full name. sortParts converts this into a topologically sorted array. + let pluginsNew = {}; // 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}...`); - 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; - defs.parts = sortParts(parts); - defs.hooks = pluginUtils.extractHooks(defs.parts, 'hooks', exports.pathNormalization); - defs.loaded = true; - await Promise.all(Object.keys(defs.plugins).map(async (p) => { + setPlugins(pluginsNew) + setParts(sortParts(partsNew)) + setHooks(pluginUtils.extractHooks(parts, 'hooks', pathNormalization)); + setLoaded(true) + await Promise.all(Object.keys(plugins).map(async (p) => { const logger = log4js.getLogger(`plugin:${p}`); await hooks.aCallAll(`init_${p}`, {logger}); })); }; -exports.getPackages = async () => { +export const getPackages = async () => { logger.info('Running npm to get a list of installed plugins...'); // Notes: // * 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 {dependencies = {}} = JSON.parse(await runCmd(cmd, {stdio: [null, 'string']})); await Promise.all(Object.entries(dependencies).map(async ([pkg, info]) => { - if (!pkg.startsWith(exports.prefix)) { + if (!pkg.startsWith(prefix)) { delete dependencies[pkg]; return; } - info.realPath = await fs.realpath(info.path); + info.realPath = await promises.realpath(info.path); })); return dependencies; }; @@ -126,7 +126,7 @@ exports.getPackages = async () => { const loadPlugin = async (packages, pluginName, plugins, parts) => { const pluginPath = path.resolve(packages[pluginName].path, 'ep.json'); try { - const data = await fs.readFile(pluginPath); + const data = await promises.readFile(pluginPath); try { const plugin = JSON.parse(data); plugin.package = packages[pluginName]; diff --git a/src/static/js/pluginfw/shared.js b/src/static/js/pluginfw/shared.js index 2c81ccd81..bc85a74d7 100644 --- a/src/static/js/pluginfw/shared.js +++ b/src/static/js/pluginfw/shared.js @@ -1,6 +1,6 @@ 'use strict'; -const defs = require('./plugin_defs'); +import {parts} from './plugin_defs.js' const disabledHookReasons = { hooks: { @@ -33,7 +33,7 @@ const loadFn = (path, hookName) => { return fn; }; -const extractHooks = (parts, hookSetName, normalizer) => { +export const extractHooks = (parts, hookSetName, normalizer) => { const hooks = {}; for (const part of parts) { for (const [hookName, regHookFnName] of Object.entries(part[hookSetName] || {})) { @@ -74,7 +74,6 @@ const extractHooks = (parts, hookSetName, normalizer) => { return hooks; }; -exports.extractHooks = extractHooks; /* * Returns an array containing the names of the installed client-side plugins @@ -88,8 +87,8 @@ exports.extractHooks = extractHooks; * No plugins: [] * Some plugins: [ 'ep_adminpads', 'ep_add_buttons', 'ep_activepads' ] */ -exports.clientPluginNames = () => { - const clientPluginNames = defs.parts +export const clientPluginNames = () => { + const clientPluginNames = parts .filter((part) => Object.prototype.hasOwnProperty.call(part, 'client_hooks')) .map((part) => `plugin-${part.plugin}`); return [...new Set(clientPluginNames)]; diff --git a/src/static/js/pluginfw/tsort.js b/src/static/js/pluginfw/tsort.js index 117f8555c..d0ca81481 100644 --- a/src/static/js/pluginfw/tsort.js +++ b/src/static/js/pluginfw/tsort.js @@ -111,3 +111,4 @@ if (typeof exports === 'object' && exports === this) { module.exports = tsort; if (process.argv[1] === __filename) tsortTest(); } +export default tsort \ No newline at end of file