From 3c2129b1ccd14817f28b39b2d5c30a1297aa66b2 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Fri, 23 Jun 2023 18:57:36 +0200 Subject: [PATCH] Rewrote server in typescript. --- src/.gitignore | 3 +- src/node/db/API.ts | 16 +- src/node/db/AuthorManager.ts | 4 +- src/node/db/GroupManager.ts | 2 +- src/node/db/Pad.ts | 55 ++++--- src/node/db/PadManager.ts | 2 +- src/node/db/ReadOnlyManager.ts | 2 +- src/node/db/SecurityManager.ts | 4 +- src/node/handler/APIHandler.ts | 6 +- src/node/handler/ImportHandler.ts | 8 +- src/node/handler/PadMessageHandler.ts | 18 +-- src/node/handler/SocketIORouter.ts | 8 +- src/node/hooks/{express.js => express.ts} | 142 +++++++++++------- src/node/hooks/express/importexport.ts | 4 +- .../{padurlsanitize.js => padurlsanitize.ts} | 6 +- .../express/{socketio.js => socketio.ts} | 30 ++-- .../{specialpages.js => specialpages.ts} | 42 +++--- .../hooks/express/{static.js => static.ts} | 12 +- src/node/hooks/express/{tests.js => tests.ts} | 22 +-- .../express/{webaccess.js => webaccess.ts} | 60 ++++---- src/node/hooks/{i18n.js => i18n.ts} | 36 ++--- src/node/models/CMDOptions.ts | 11 ++ src/node/models/LogLevel.ts | 1 + src/node/models/PadDiffModel.ts | 3 + src/node/models/Presession.ts | 5 + src/node/models/SessionSocketModel.ts | 2 + src/node/models/UserIndexedModel.ts | 5 + src/node/server.ts | 2 + src/node/utils/{Abiword.js => Abiword.ts} | 25 +-- .../{AbsolutePaths.js => AbsolutePaths.ts} | 15 +- src/node/utils/{Cli.js => Cli.ts} | 3 +- .../{ExportEtherpad.js => ExportEtherpad.ts} | 19 ++- .../{ExportHelper.js => ExportHelper.ts} | 19 ++- .../utils/{ExportHtml.js => ExportHtml.ts} | 36 ++--- src/node/utils/{ExportTxt.js => ExportTxt.ts} | 7 +- .../{ImportEtherpad.js => ImportEtherpad.ts} | 26 ++-- .../utils/{ImportHtml.js => ImportHtml.ts} | 12 +- .../utils/{LibreOffice.js => LibreOffice.ts} | 23 +-- src/node/utils/{Minify.js => Minify.ts} | 43 +++--- .../{MinifyWorker.js => MinifyWorker.ts} | 13 +- src/node/utils/Settings.ts | 121 ++++++++------- src/node/utils/{Stream.js => Stream.ts} | 6 +- src/node/utils/{TidyHtml.js => TidyHtml.ts} | 11 +- ...ng_middleware.js => caching_middleware.ts} | 42 ++++-- .../utils/{customError.js => customError.ts} | 6 +- src/node/utils/{padDiff.js => padDiff.ts} | 31 ++-- .../utils/{path_exists.js => path_exists.ts} | 8 +- src/node/utils/{promises.js => promises.ts} | 10 +- .../{randomstring.js => randomstring.ts} | 0 src/node/utils/{run_cmd.js => run_cmd.ts} | 53 ++++--- ...anitizePathname.js => sanitizePathname.ts} | 6 +- src/node/utils/{toolbar.js => toolbar.ts} | 6 +- src/package-lock.json | 23 +++ src/package.json | 12 +- .../js/{ace2_common.js => ace2_common.ts} | 19 +-- src/static/js/{pad_utils.js => pad_utils.ts} | 50 +++--- src/tsconfig.json | 2 + 57 files changed, 668 insertions(+), 490 deletions(-) rename src/node/hooks/{express.js => express.ts} (71%) rename src/node/hooks/express/{padurlsanitize.js => padurlsanitize.ts} (83%) rename src/node/hooks/express/{socketio.js => socketio.ts} (88%) rename src/node/hooks/express/{specialpages.js => specialpages.ts} (63%) rename src/node/hooks/express/{static.js => static.ts} (86%) rename src/node/hooks/express/{tests.js => tests.ts} (81%) rename src/node/hooks/express/{webaccess.js => webaccess.ts} (85%) rename src/node/hooks/{i18n.js => i18n.ts} (79%) create mode 100644 src/node/models/CMDOptions.ts create mode 100644 src/node/models/LogLevel.ts create mode 100644 src/node/models/PadDiffModel.ts create mode 100644 src/node/models/Presession.ts create mode 100644 src/node/models/UserIndexedModel.ts rename src/node/utils/{Abiword.js => Abiword.ts} (80%) rename src/node/utils/{AbsolutePaths.js => AbsolutePaths.ts} (94%) rename src/node/utils/{Cli.js => Cli.ts} (96%) rename src/node/utils/{ExportEtherpad.js => ExportEtherpad.ts} (84%) rename src/node/utils/{ExportHelper.js => ExportHelper.ts} (85%) rename src/node/utils/{ExportHtml.js => ExportHtml.ts} (95%) rename src/node/utils/{ExportTxt.js => ExportTxt.ts} (97%) rename src/node/utils/{ImportEtherpad.js => ImportEtherpad.ts} (86%) rename src/node/utils/{ImportHtml.js => ImportHtml.ts} (92%) rename src/node/utils/{LibreOffice.js => LibreOffice.ts} (91%) rename src/node/utils/{Minify.js => Minify.ts} (91%) rename src/node/utils/{MinifyWorker.js => MinifyWorker.ts} (80%) rename src/node/utils/{Stream.js => Stream.ts} (98%) rename src/node/utils/{TidyHtml.js => TidyHtml.ts} (83%) rename src/node/utils/{caching_middleware.js => caching_middleware.ts} (90%) rename src/node/utils/{customError.js => customError.ts} (87%) rename src/node/utils/{padDiff.js => padDiff.ts} (94%) rename src/node/utils/{path_exists.js => path_exists.ts} (71%) rename src/node/utils/{promises.js => promises.ts} (93%) rename src/node/utils/{randomstring.js => randomstring.ts} (100%) rename src/node/utils/{run_cmd.js => run_cmd.ts} (81%) rename src/node/utils/{sanitizePathname.js => sanitizePathname.ts} (94%) rename src/node/utils/{toolbar.js => toolbar.ts} (98%) rename src/static/js/{ace2_common.js => ace2_common.ts} (78%) rename src/static/js/{pad_utils.js => pad_utils.ts} (92%) diff --git a/src/.gitignore b/src/.gitignore index 1521c8b76..81cd8b481 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1 +1,2 @@ -dist +dist/ +node_modules diff --git a/src/node/db/API.ts b/src/node/db/API.ts index 99b19498f..d434eb556 100644 --- a/src/node/db/API.ts +++ b/src/node/db/API.ts @@ -21,7 +21,7 @@ import Changeset from '../../static/js/Changeset'; import ChatMessage from '../../static/js/ChatMessage'; -import CustomError from '../utils/customError'; +import {CustomError} from '../utils/customError'; import {doesPadExist, getPad, isValidPadId, listAllPads} from './PadManager'; import { handleCustomMessage, @@ -40,11 +40,11 @@ import { } from './GroupManager'; import {createAuthor, createAuthorIfNotExistsFor, getAuthorName, listPadsOfAuthor} from './AuthorManager'; import {} from './SessionManager'; -import exportHtml from '../utils/ExportHtml'; -import exportTxt from '../utils/ExportTxt'; -import importHtml from '../utils/ImportHtml'; +import {getTXTFromAtext} from '../utils/ExportTxt'; +import {setPadHTML} from '../utils/ImportHtml'; const cleanText = require('./Pad').cleanText; -import PadDiff from '../utils/padDiff'; +import {PadDiff} from '../utils/padDiff'; +import {getPadHTMLDocument} from "../utils/ExportHtml"; /* ******************** * GROUP FUNCTIONS **** @@ -192,7 +192,7 @@ export const getText = async (padID, rev) => { } // the client wants the latest text, lets return it to him - const text = exportTxt.getTXTFromAtext(pad, pad.atext); + const text = getTXTFromAtext(pad, pad.atext); return {text}; }; @@ -263,7 +263,7 @@ export const getHTML = async (padID, rev) => { } // get the html of this revision - let html = await exportHtml.getPadHTML(pad, rev); + let html = await getPadHTMLDocument(pad, rev); // wrap the HTML html = `${html}`; @@ -289,7 +289,7 @@ export const setHTML = async (padID, html, authorId = '') => { // add a new changeset with the new html to the pad try { - await importHtml.setPadHTML(pad, cleanText(html), authorId); + await setPadHTML(pad, cleanText(html), authorId); } catch (e) { throw new CustomError('HTML is malformed', 'apierror'); } diff --git a/src/node/db/AuthorManager.ts b/src/node/db/AuthorManager.ts index fd825bba6..a3f5db0dc 100644 --- a/src/node/db/AuthorManager.ts +++ b/src/node/db/AuthorManager.ts @@ -20,7 +20,7 @@ */ import {db} from './DB'; -import CustomError from '../utils/customError'; +import {CustomError} from '../utils/customError'; import hooks from '../../static/js/pluginfw/hooks.js'; const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils'); @@ -283,7 +283,7 @@ export const addPad = async (authorID: string, padID: string) => { * @param {String} authorID The id of the author * @param {String} padID The id of the pad the author contributes to */ -export const removePad = async (authorID: string, padID: string) => { +export const removePad = async (authorID: string, padID?: string) => { const author = await db.get(`globalAuthor:${authorID}`); if (author == null) return; diff --git a/src/node/db/GroupManager.ts b/src/node/db/GroupManager.ts index 6d4e2d0e1..0be2205d8 100644 --- a/src/node/db/GroupManager.ts +++ b/src/node/db/GroupManager.ts @@ -19,7 +19,7 @@ * limitations under the License. */ -import CustomError from '../utils/customError'; +import {CustomError} from '../utils/customError'; const randomString = require('../../static/js/pad_utils').randomString; import {db} from './DB'; import {doesPadExist, getPad} from './PadManager'; diff --git a/src/node/db/Pad.ts b/src/node/db/Pad.ts index 9f1742705..be7ac0155 100644 --- a/src/node/db/Pad.ts +++ b/src/node/db/Pad.ts @@ -7,22 +7,21 @@ import AttributeMap from '../../static/js/AttributeMap'; import Changeset from '../../static/js/Changeset'; import ChatMessage from '../../static/js/ChatMessage'; import {AttributePool} from '../../static/js/AttributePool'; -import Stream from '../utils/Stream'; +import {Stream} from '../utils/Stream'; import assert, {strict} from 'assert' import {db} from './DB'; import {defaultPadText} from '../utils/Settings'; import {addPad, getAuthorColorId, getAuthorName, getColorPalette, removePad} from './AuthorManager'; import {Revision} from "../models/Revision"; -const padManager = require('./PadManager'); -const padMessageHandler = require('../handler/PadMessageHandler'); -const groupManager = require('./GroupManager'); -const CustomError = require('../utils/customError'); -const readOnlyManager = require('./ReadOnlyManager'); -const randomString = require('../utils/randomstring'); -const hooks = require('../../static/js/pluginfw/hooks'); -const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils'); -const promises = require('../utils/promises'); - +import {doesPadExist, getPad} from './PadManager'; +import {kickSessionsFromPad} from '../handler/PadMessageHandler'; +import {doesGroupExist} from './GroupManager'; +import {CustomError} from '../utils/customError'; +import {getReadOnlyId} from './ReadOnlyManager'; +import {randomString} from '../utils/randomstring'; +import hooks from '../../static/js/pluginfw/hooks'; +import {timesLimit} from '../utils/promises'; +import {padutils} from '../../static/js/pad_utils'; /** * Copied from the Etherpad source code. It converts Windows line breaks to Unix * line breaks and convert Tabs to spaces @@ -114,11 +113,11 @@ export class Pad { pad: this, authorId, get author() { - warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); + padutils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); return this.authorId; }, set author(authorId) { - warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); + padutils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`); this.authorId = authorId; }, ...this.head === 0 ? {} : { @@ -403,16 +402,16 @@ export class Pad { for (const p of new Stream(promises).batch(100).buffer(99)) await p; // Initialize the new pad (will update the listAllPads cache) - const dstPad = await padManager.getPad(destinationID, null); + const dstPad = await getPad(destinationID, null); // let the plugins know the pad was copied await hooks.aCallAll('padCopy', { get originalPad() { - warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead'); + padutils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead'); return this.srcPad; }, get destinationID() { - warnDeprecated( + padutils.warnDeprecated( 'padCopy destinationID context property is deprecated; use dstPad.id instead'); return this.dstPad.id; }, @@ -428,7 +427,7 @@ export class Pad { if (destinationID.indexOf('$') >= 0) { destGroupID = destinationID.split('$')[0]; - const groupExists = await groupManager.doesGroupExist(destGroupID); + const groupExists = await doesGroupExist(destGroupID); // group does not exist if (!groupExists) { @@ -440,7 +439,7 @@ export class Pad { async removePadIfForceIsTrueAndAlreadyExist(destinationID, force) { // if the pad exists, we should abort, unless forced. - const exists = await padManager.doesPadExist(destinationID); + const exists = await doesPadExist(destinationID); // allow force to be a string if (typeof force === 'string') { @@ -456,7 +455,7 @@ export class Pad { } // exists and forcing - const pad = await padManager.getPad(destinationID); + const pad = await getPad(destinationID); await pad.remove(); } } @@ -485,7 +484,7 @@ export class Pad { } // initialize the pad with a new line to avoid getting the defaultText - const dstPad = await padManager.getPad(destinationID, '\n', authorId); + const dstPad = await getPad(destinationID, '\n', authorId); dstPad.pool = this.pool.clone(); const oldAText = this.atext; @@ -509,11 +508,11 @@ export class Pad { await hooks.aCallAll('padCopy', { get originalPad() { - warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead'); + padutils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead'); return this.srcPad; }, get destinationID() { - warnDeprecated( + padutils.warnDeprecated( 'padCopy destinationID context property is deprecated; use dstPad.id instead'); return this.dstPad.id; }, @@ -529,7 +528,7 @@ export class Pad { const p = []; // kick everyone from this pad - padMessageHandler.kickSessionsFromPad(padID); + kickSessionsFromPad(padID); // delete all relations - the original code used async.parallel but // none of the operations except getting the group depended on callbacks @@ -550,18 +549,18 @@ export class Pad { } // remove the readonly entries - p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID) => { + p.push(getReadOnlyId(padID).then(async (readonlyID) => { await db.remove(`readonly2pad:${readonlyID}`); })); p.push(db.remove(`pad2readonly:${padID}`)); // delete all chat messages - p.push(promises.timesLimit(this.chatHead + 1, 500, async (i) => { + p.push(timesLimit(this.chatHead + 1, 500, async (i) => { await this.db.remove(`pad:${this.id}:chat:${i}`, null); })); // delete all revisions - p.push(promises.timesLimit(this.head + 1, 500, async (i) => { + p.push(timesLimit(this.head + 1, 500, async (i) => { await this.db.remove(`pad:${this.id}:revs:${i}`, null); })); @@ -571,10 +570,10 @@ export class Pad { }); // delete the pad entry and delete pad from padManager - p.push(padManager.removePad(padID)); + p.push(removePad(padID)); p.push(hooks.aCallAll('padRemove', { get padID() { - warnDeprecated('padRemove padID context property is deprecated; use pad.id instead'); + padutils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead'); return this.pad.id; }, pad: this, diff --git a/src/node/db/PadManager.ts b/src/node/db/PadManager.ts index 568c19f83..a48473546 100644 --- a/src/node/db/PadManager.ts +++ b/src/node/db/PadManager.ts @@ -19,7 +19,7 @@ * limitations under the License. */ -import CustomError from '../utils/customError'; +import {CustomError} from '../utils/customError'; import {Pad} from './Pad'; import {db} from './DB'; diff --git a/src/node/db/ReadOnlyManager.ts b/src/node/db/ReadOnlyManager.ts index fe763cd9a..8d5a036c8 100644 --- a/src/node/db/ReadOnlyManager.ts +++ b/src/node/db/ReadOnlyManager.ts @@ -21,7 +21,7 @@ import {db} from './DB'; -import randomString from '../utils/randomstring'; +import {randomString} from '../utils/randomstring'; /** diff --git a/src/node/db/SecurityManager.ts b/src/node/db/SecurityManager.ts index dcebdb897..ca894b5e5 100644 --- a/src/node/db/SecurityManager.ts +++ b/src/node/db/SecurityManager.ts @@ -31,7 +31,7 @@ import {findAuthorID} from "./SessionManager"; import {editOnly, loadTest, requireAuthentication, requireSession} from "../utils/Settings"; -import webaccess from "../hooks/express/webaccess"; +import {normalizeAuthzLevel} from "../hooks/express/webaccess"; import log4js from "log4js"; @@ -92,7 +92,7 @@ export const checkAccess = async (padID, sessionCookie, token, userSettings) => // Note: userSettings.padAuthorizations should still be populated even if // settings.requireAuthorization is false. const padAuthzs = userSettings.padAuthorizations || {}; - const level = webaccess.normalizeAuthzLevel(padAuthzs[padID]); + const level = normalizeAuthzLevel(padAuthzs[padID]); if (!level) { authLogger.debug('access denied: unauthorized'); return DENY; diff --git a/src/node/handler/APIHandler.ts b/src/node/handler/APIHandler.ts index d1ed23fb3..5b485f33a 100644 --- a/src/node/handler/APIHandler.ts +++ b/src/node/handler/APIHandler.ts @@ -19,12 +19,12 @@ * limitations under the License. */ -import absolutePaths from '../utils/AbsolutePaths'; +import {makeAbsolute} from '../utils/AbsolutePaths'; import fs from 'fs'; import * as api from '../db/API'; import log4js from 'log4js'; import {sanitizePadId} from '../db/PadManager'; -import randomString from '../utils/randomstring'; +import {randomString} from '../utils/randomstring'; const argv = require('../utils/Cli').argv; const createHTTPError = require('http-errors'); @@ -32,7 +32,7 @@ const apiHandlerLogger = log4js.getLogger('APIHandler'); // ensure we have an apikey let apikey = null; -const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt'); +const apikeyFilename = makeAbsolute(argv.apikey || './APIKEY.txt'); try { apikey = fs.readFileSync(apikeyFilename, 'utf8'); diff --git a/src/node/handler/ImportHandler.ts b/src/node/handler/ImportHandler.ts index 889422564..92f5e38a7 100644 --- a/src/node/handler/ImportHandler.ts +++ b/src/node/handler/ImportHandler.ts @@ -29,8 +29,8 @@ import {promises as fs} from "fs"; import {abiword, allowUnknownFileEnds, importMaxFileSize, soffice} from '../utils/Settings'; import {Formidable} from 'formidable'; import os from 'os'; -import importHtml from '../utils/ImportHtml'; -import importEtherpad from '../utils/ImportEtherpad'; +import {setPadHTML} from '../utils/ImportHtml'; +import {setPadRaw} from '../utils/ImportEtherpad'; import log4js from 'log4js'; import hooks from '../../static/js/pluginfw/hooks.js'; @@ -150,7 +150,7 @@ const doImport = async (req, res, padId, authorId) => { } const text = await fs.readFile(srcFile, 'utf8'); directDatabaseAccess = true; - await importEtherpad.setPadRaw(padId, text, authorId); + await setPadRaw(padId, text, authorId); } // convert file to html if necessary @@ -207,7 +207,7 @@ const doImport = async (req, res, padId, authorId) => { if (!directDatabaseAccess) { if (importHandledByPlugin || useConverter || fileIsHTML) { try { - await importHtml.setPadHTML(pad, text, authorId); + await setPadHTML(pad, text, authorId); } catch (err) { logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`); } diff --git a/src/node/handler/PadMessageHandler.ts b/src/node/handler/PadMessageHandler.ts index dfa80430d..5e9f0665d 100644 --- a/src/node/handler/PadMessageHandler.ts +++ b/src/node/handler/PadMessageHandler.ts @@ -58,7 +58,7 @@ import {createCollection} from '../stats'; import {strict as assert} from "assert"; import {RateLimiterMemory} from 'rate-limiter-flexible'; -import webaccess from '../hooks/express/webaccess'; +import {userCanModify} from '../hooks/express/webaccess'; import {ErrorCaused} from "../models/ErrorCaused"; import {Pad} from "../db/Pad"; import {SessionInfo} from "../models/SessionInfo"; @@ -164,7 +164,7 @@ const padChannels = new Channels((ch, {socket, message}) => handleUserChanges(so * This Method is called by server.js to tell the message handler on which socket it should send * @param socket_io The Socket */ -exports.setSocketIO = (socket_io) => { +export const setSocketIO = (socket_io) => { socketio = socket_io; }; @@ -172,7 +172,7 @@ exports.setSocketIO = (socket_io) => { * Handles the connection of a new user * @param socket the socket.io Socket object for the new connection from the client */ -exports.handleConnect = (socket) => { +export const handleConnect = (socket) => { createCollection.meter('connects').mark(); // Initialize sessioninfos for this new session @@ -186,7 +186,7 @@ exports.handleConnect = (socket) => { /** * Kicks all sessions from a pad */ -exports.kickSessionsFromPad = (padID) => { +export const kickSessionsFromPad = (padID) => { if (typeof socketio.sockets.clients !== 'function') return; // skip if there is nobody on this pad @@ -200,7 +200,7 @@ exports.kickSessionsFromPad = (padID) => { * Handles the disconnection of a user * @param socket the socket.io Socket object for the client */ -exports.handleDisconnect = async (socket) => { +export const handleDisconnect = async (socket) => { createCollection.meter('disconnects').mark(); const session = sessioninfos[socket.id]; delete sessioninfos[socket.id]; @@ -238,7 +238,7 @@ exports.handleDisconnect = async (socket) => { * @param socket the socket.io Socket object for the client * @param message the message from the client */ -exports.handleMessage = async (socket, message) => { +export const handleMessage = async (socket, message) => { const env = process.env.NODE_ENV || 'development'; if (env === 'production') { @@ -272,7 +272,7 @@ exports.handleMessage = async (socket, message) => { thisSession.padId = padIds.padId; thisSession.readOnlyPadId = padIds.readOnlyPadId; thisSession.readonly = - padIds.readonly || !webaccess.userCanModify(thisSession.auth.padID, socket.client.request); + padIds.readonly || !userCanModify(thisSession.auth.padID, socket.client.request); } // Outside of the checks done by this function, message.padId must not be accessed because it is // too easy to introduce a security vulnerability that allows malicious users to read or modify @@ -416,7 +416,7 @@ const handleSaveRevisionMessage = async (socket, message) => { * @param msg {Object} the message we're sending * @param sessionID {string} the socketIO session to which we're sending this message */ -exports.handleCustomObjectMessage = (msg, sessionID) => { +export const handleCustomObjectMessage = (msg, sessionID) => { if (msg.data.type === 'CUSTOM') { if (sessionID) { // a sessionID is targeted: directly to this sessionID @@ -684,7 +684,7 @@ const handleUserChanges = async (socket, message) => { socket.json.send({type: 'COLLABROOM', data: {type: 'ACCEPT_COMMIT', newRev}}); thisSession.rev = newRev; if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev); - await exports.updatePadClients(pad); + await updatePadClients(pad); } catch (err) { socket.json.send({disconnect: 'badChangeset'}); createCollection.meter('failedChangesets').mark(); diff --git a/src/node/handler/SocketIORouter.ts b/src/node/handler/SocketIORouter.ts index 73594bfb0..9f7875566 100644 --- a/src/node/handler/SocketIORouter.ts +++ b/src/node/handler/SocketIORouter.ts @@ -39,18 +39,18 @@ let io; /** * adds a component */ -exports.addComponent = (moduleName, module) => { - if (module == null) return exports.deleteComponent(moduleName); +export const addComponent = (moduleName, module) => { + if (module == null) return deleteComponent(moduleName); components[moduleName] = module; module.setSocketIO(io); }; -exports.deleteComponent = (moduleName) => { delete components[moduleName]; }; +export const deleteComponent = (moduleName) => { delete components[moduleName]; }; /** * sets the socket.io and adds event functions for routing */ -exports.setSocketIO = (_io) => { +export const setSocketIO = (_io) => { io = _io; io.sockets.on('connection', (socket) => { diff --git a/src/node/hooks/express.js b/src/node/hooks/express.ts similarity index 71% rename from src/node/hooks/express.js rename to src/node/hooks/express.ts index 9c42fd6d8..74f5834d2 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.ts @@ -1,34 +1,57 @@ 'use strict'; -const _ = require('underscore'); -const cookieParser = require('cookie-parser'); -const events = require('events'); -const express = require('express'); -const expressSession = require('express-session'); -const fs = require('fs'); -const hooks = require('../../static/js/pluginfw/hooks'); -const log4js = require('log4js'); -const SessionStore = require('../db/SessionStore'); -const settings = require('../utils/Settings'); -const stats = require('../stats'); -const util = require('util'); -const webaccess = require('./express/webaccess'); +import _ from 'underscore'; +import cookieParser from 'cookie-parser'; +import events from "events"; + +import express from "express"; + +import fs from "fs"; + +import expressSession from "express-session"; + +import hooks from "../../static/js/pluginfw/hooks"; + +import log4js from "log4js"; + +import SessionStore from "../db/SessionStore"; + +import { + cookie, + exposeVersion, + getEpVersion, + getGitCommit, + ip, + loglevel, + port, + sessionKey, + ssl, sslKeys, + trustProxy, + users +} from "../utils/Settings"; + +import {createCollection} from "../stats"; + +import util from "util"; + +import {checkAccess, checkAccess2} from "./express/webaccess"; +import {Socket} from "net"; const logger = log4js.getLogger('http'); let serverName; let sessionStore; -const sockets = new Set(); +const sockets = new Set(); const socketsEvents = new events.EventEmitter(); -const startTime = stats.settableGauge('httpStartTime'); - -exports.server = null; +const startTime = createCollection.settableGauge('httpStartTime'); +export let server = null; +export let sessionMiddleware; const closeServer = async () => { - if (exports.server != null) { + if (server != null) { logger.info('Closing HTTP server...'); // Call exports.server.close() to reject new connections but don't await just yet because the // Promise won't resolve until all preexisting connections are closed. - const p = util.promisify(exports.server.close.bind(exports.server))(); + const p = util.promisify(server.close.bind(server))(); await hooks.aCallAll('expressCloseServer'); // Give existing connections some time to close on their own before forcibly terminating. The // time should be long enough to avoid interrupting most preexisting transmissions but short @@ -47,7 +70,7 @@ const closeServer = async () => { } await p; clearTimeout(timeout); - exports.server = null; + server = null; startTime.setValue(0); logger.info('HTTP server closed'); } @@ -55,24 +78,24 @@ const closeServer = async () => { sessionStore = null; }; -exports.createServer = async () => { +export const createServer = async () => { console.log('Report bugs at https://github.com/ether/etherpad-lite/issues'); - serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`; + serverName = `Etherpad ${getGitCommit()} (https://etherpad.org)`; - console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); + console.log(`Your Etherpad version is ${getEpVersion()} (${getGitCommit()})`); - await exports.restartServer(); + await restartServer(); - if (settings.ip === '') { + if (ip.length===0) { // using Unix socket for connectivity - console.log(`You can access your Etherpad instance using the Unix socket at ${settings.port}`); + console.log(`You can access your Etherpad instance using the Unix socket at ${port}`); } else { - console.log(`You can access your Etherpad instance at http://${settings.ip}:${settings.port}/`); + console.log(`You can access your Etherpad instance at http://${ip}:${port}/`); } - if (!_.isEmpty(settings.users)) { - console.log(`The plugin admin page is at http://${settings.ip}:${settings.port}/admin/plugins`); + if (!_.isEmpty(users)) { + console.log(`The plugin admin page is at http://${ip}:${port}/admin/plugins`); } else { console.warn('Admin username and password not set in settings.json. ' + 'To access admin please uncomment and edit "users" in settings.json'); @@ -87,39 +110,40 @@ exports.createServer = async () => { } }; -exports.restartServer = async () => { +export const restartServer = async () => { await closeServer(); const app = express(); // New syntax for express v3 - if (settings.ssl) { + if (ssl) { console.log('SSL -- enabled'); - console.log(`SSL -- server key file: ${settings.ssl.key}`); - console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`); + console.log(`SSL -- server key file: ${sslKeys.key}`); + console.log(`SSL -- Certificate Authority's certificate file: ${sslKeys.cert}`); const options = { - key: fs.readFileSync(settings.ssl.key), - cert: fs.readFileSync(settings.ssl.cert), + key: fs.readFileSync(sslKeys.key), + cert: fs.readFileSync(sslKeys.cert), + ca: undefined }; - if (settings.ssl.ca) { + if (sslKeys.ca) { options.ca = []; - for (let i = 0; i < settings.ssl.ca.length; i++) { - const caFileName = settings.ssl.ca[i]; + for (let i = 0; i < sslKeys.ca.length; i++) { + const caFileName = sslKeys.ca[i]; options.ca.push(fs.readFileSync(caFileName)); } } const https = require('https'); - exports.server = https.createServer(options, app); + server = https.createServer(options, app); } else { const http = require('http'); - exports.server = http.createServer(app); + server = http.createServer(app); } app.use((req, res, next) => { // res.header("X-Frame-Options", "deny"); // breaks embedded pads - if (settings.ssl) { + if (ssl) { // we use SSL res.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); } @@ -138,14 +162,14 @@ exports.restartServer = async () => { res.header('Referrer-Policy', 'same-origin'); // send git version in the Server response header if exposeVersion is true. - if (settings.exposeVersion) { + if (exposeVersion) { res.header('Server', serverName); } next(); }); - if (settings.trustProxy) { + if (trustProxy) { /* * If 'trust proxy' === true, the client’s IP address in req.ip will be the * left-most entry in the X-Forwarded-* header. @@ -157,8 +181,10 @@ exports.restartServer = async () => { // Measure response time app.use((req, res, next) => { - const stopWatch = stats.timer('httpRequests').start(); + const stopWatch = createCollection.timer('httpRequests').start(); const sendFn = res.send.bind(res); + // FIXME Check if this is still needed + // @ts-ignore res.send = (...args) => { stopWatch.end(); sendFn(...args); }; next(); }); @@ -167,20 +193,20 @@ exports.restartServer = async () => { // starts listening to requests as reported in issue #158. Not installing the log4js connect // logger when the log level has a higher severity than INFO since it would not log at that level // anyway. - if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) { + if (!(loglevel === 'WARN') && loglevel === 'ERROR') { app.use(log4js.connectLogger(logger, { - level: log4js.levels.DEBUG, + level: loglevel, format: ':status, :method :url', })); } - app.use(cookieParser(settings.sessionKey, {})); + app.use(cookieParser(sessionKey, {})); - sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval); - exports.sessionMiddleware = expressSession({ + sessionStore = new SessionStore(cookie.sessionRefreshInterval); +sessionMiddleware = expressSession({ propagateTouch: true, rolling: true, - secret: settings.sessionKey, + secret: sessionKey, store: sessionStore, resave: false, saveUninitialized: false, @@ -188,8 +214,8 @@ exports.restartServer = async () => { // cleaner :) name: 'express_sid', cookie: { - maxAge: settings.cookie.sessionLifetime || null, // Convert 0 to null. - sameSite: settings.cookie.sameSite, + maxAge: cookie.sessionLifetime || null, // Convert 0 to null. + sameSite: cookie.sameSite, // The automatic express-session mechanism for determining if the application is being served // over ssl is similar to the one used for setting the language cookie, which check if one of @@ -215,15 +241,15 @@ exports.restartServer = async () => { // middleware. This allows plugins to avoid creating an express-session record in the database // when it is not needed (e.g., public static content). await hooks.aCallAll('expressPreSession', {app}); - app.use(exports.sessionMiddleware); + app.use(sessionMiddleware); - app.use(webaccess.checkAccess); + app.use(checkAccess2); await Promise.all([ hooks.aCallAll('expressConfigure', {app}), - hooks.aCallAll('expressCreateServer', {app, server: exports.server}), + hooks.aCallAll('expressCreateServer', {app, server: server}), ]); - exports.server.on('connection', (socket) => { + server.on('connection', (socket) => { sockets.add(socket); socketsEvents.emit('updated'); socket.on('close', () => { @@ -231,11 +257,11 @@ exports.restartServer = async () => { socketsEvents.emit('updated'); }); }); - await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip); + await util.promisify(server.listen).bind(server)(port, ip); startTime.setValue(Date.now()); logger.info('HTTP server listening for connections'); }; -exports.shutdown = async (hookName, context) => { +export const shutdown = async (hookName, context) => { await closeServer(); }; diff --git a/src/node/hooks/express/importexport.ts b/src/node/hooks/express/importexport.ts index 5df195599..e5d096b76 100644 --- a/src/node/hooks/express/importexport.ts +++ b/src/node/hooks/express/importexport.ts @@ -8,7 +8,7 @@ import {doesPadExist} from '../../db/PadManager'; import {getPadId, isReadOnlyId} from '../../db/ReadOnlyManager'; import rateLimit from 'express-rate-limit'; import {checkAccess} from '../../db/SecurityManager'; -import webaccess from './webaccess'; +import {userCanModify} from './webaccess'; exports.expressCreateServer = (hookName, args, cb) => { importExportRateLimiting.onLimitReached = (req, res, options) => { @@ -72,7 +72,7 @@ exports.expressCreateServer = (hookName, args, cb) => { const {session: {user} = {}}:SessionSocketModel = req; const {accessStatus, authorID: authorId} = await checkAccess( req.params.pad, req.cookies.sessionID, req.cookies.token, user); - if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) { + if (accessStatus !== 'grant' || !userCanModify(req.params.pad, req)) { return res.status(403).send('Forbidden'); } await doImport2(req, res, req.params.pad, authorId); diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.ts similarity index 83% rename from src/node/hooks/express/padurlsanitize.js rename to src/node/hooks/express/padurlsanitize.ts index ff1afa477..00006bca4 100644 --- a/src/node/hooks/express/padurlsanitize.js +++ b/src/node/hooks/express/padurlsanitize.ts @@ -1,18 +1,18 @@ 'use strict'; -const padManager = require('../../db/PadManager'); +import {isValidPadId, sanitizePadId} from '../../db/PadManager'; exports.expressCreateServer = (hookName, args, cb) => { // redirects browser to the pad's sanitized url if needed. otherwise, renders the html args.app.param('pad', (req, res, next, padId) => { (async () => { // ensure the padname is valid and the url doesn't end with a / - if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) { + if (!isValidPadId(padId) || /\/$/.test(req.url)) { res.status(404).send('Such a padname is forbidden'); return; } - const sanitizedPadId = await padManager.sanitizePadId(padId); + const sanitizedPadId = await sanitizePadId(padId); if (sanitizedPadId === padId) { // the pad id was fine, so just render it diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.ts similarity index 88% rename from src/node/hooks/express/socketio.js rename to src/node/hooks/express/socketio.ts index edb679940..feed1a2fb 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.ts @@ -1,14 +1,14 @@ 'use strict'; -const events = require('events'); -const express = require('../express'); -const log4js = require('log4js'); -const proxyaddr = require('proxy-addr'); -const settings = require('../../utils/Settings'); -const socketio = require('socket.io'); -const socketIORouter = require('../../handler/SocketIORouter'); -const hooks = require('../../../static/js/pluginfw/hooks'); -const padMessageHandler = require('../../handler/PadMessageHandler'); +import events from 'events'; +import {sessionMiddleware} from '../express'; +import log4js from 'log4js'; +import proxyaddr from 'proxy-addr'; +import {socketIo, socketTransportProtocols, trustProxy} from '../../utils/Settings'; +import socketio from 'socket.io'; +import {addComponent, setSocketIO} from '../../handler/SocketIORouter'; +import hooks from '../../../static/js/pluginfw/hooks'; +import * as padMessageHandler from '../../handler/PadMessageHandler'; let io; const logger = log4js.getLogger('socket.io'); @@ -52,7 +52,7 @@ exports.expressCreateServer = (hookName, args, cb) => { // transports in this list at once // e.g. XHR is disabled in IE by default, so in IE it should use jsonp-polling io = socketio({ - transports: settings.socketTransportProtocols, + transports: socketTransportProtocols, }).listen(args.server, { /* * Do not set the "io" cookie. @@ -74,7 +74,7 @@ exports.expressCreateServer = (hookName, args, cb) => { * https://github.com/socketio/socket.io/issues/2276#issuecomment-147184662 (not totally true, actually, see above) */ cookie: false, - maxHttpBufferSize: settings.socketIo.maxHttpBufferSize, + maxHttpBufferSize: socketIo.maxHttpBufferSize, }); io.on('connect', (socket) => { @@ -90,7 +90,7 @@ exports.expressCreateServer = (hookName, args, cb) => { const req = socket.request; // Express sets req.ip but socket.io does not. Replicate Express's behavior here. if (req.ip == null) { - if (settings.trustProxy) { + if (trustProxy) { req.ip = proxyaddr(req, args.app.get('trust proxy fn')); } else { req.ip = socket.handshake.address; @@ -102,7 +102,7 @@ exports.expressCreateServer = (hookName, args, cb) => { req.headers.cookie = socket.handshake.query.cookie; } // See: https://socket.io/docs/faq/#Usage-with-express-session - express.sessionMiddleware(req, {}, next); + sessionMiddleware(req, {}, next); }); io.use((socket, next) => { @@ -130,8 +130,8 @@ exports.expressCreateServer = (hookName, args, cb) => { // if(settings.minify) io.enable('browser client minification'); // Initialize the Socket.IO Router - socketIORouter.setSocketIO(io); - socketIORouter.addComponent('pad', padMessageHandler); + setSocketIO(io); + addComponent('pad', padMessageHandler); hooks.callAll('socketio', {app: args.app, io, server: args.server}); diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.ts similarity index 63% rename from src/node/hooks/express/specialpages.js rename to src/node/hooks/express/specialpages.ts index 4f41d8cd0..8b934d297 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.ts @@ -1,14 +1,14 @@ 'use strict'; -const path = require('path'); -const eejs = require('../../eejs'); -const fs = require('fs'); +import path from 'path'; +import {required} from '../../eejs'; +import fs from 'fs'; const fsp = fs.promises; -const toolbar = require('../../utils/toolbar'); -const hooks = require('../../../static/js/pluginfw/hooks'); -const settings = require('../../utils/Settings'); -const util = require('util'); -const webaccess = require('./webaccess'); +import {} from '../../utils/toolbar'; +import hooks from '../../../static/js/pluginfw/hooks'; +import {favicon, getEpVersion, maxAge, root, skinName} from '../../utils/Settings'; +import util from 'util'; +import {userCanModify} from './webaccess'; exports.expressPreSession = async (hookName, {app}) => { // This endpoint is intended to conform to: @@ -17,25 +17,25 @@ exports.expressPreSession = async (hookName, {app}) => { res.set('Content-Type', 'application/health+json'); res.json({ status: 'pass', - releaseId: settings.getEpVersion(), + releaseId: getEpVersion(), }); }); app.get('/stats', (req, res) => { - res.json(require('../../stats').toJSON()); + res.json(required('../../stats').toJSON()); }); app.get('/javascript', (req, res) => { - res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req})); + res.send(required('ep_etherpad-lite/templates/javascript.html', {req})); }); app.get('/robots.txt', (req, res) => { let filePath = - path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt'); + path.join(root, 'src', 'static', 'skins', skinName, 'robots.txt'); res.sendFile(filePath, (err) => { // there is no custom robots.txt, send the default robots.txt which dissallows all if (err) { - filePath = path.join(settings.root, 'src', 'static', 'robots.txt'); + filePath = path.join(root, 'src', 'static', 'robots.txt'); res.sendFile(filePath); } }); @@ -44,9 +44,9 @@ exports.expressPreSession = async (hookName, {app}) => { app.get('/favicon.ico', (req, res, next) => { (async () => { const fns = [ - ...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []), - path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'), - path.join(settings.root, 'src', 'static', 'favicon.ico'), + ...(favicon ? [path.resolve(root, favicon)] : []), + path.join(root, 'src', 'static', 'skins', skinName, 'favicon.ico'), + path.join(root, 'src', 'static', 'favicon.ico'), ]; for (const fn of fns) { try { @@ -54,7 +54,7 @@ exports.expressPreSession = async (hookName, {app}) => { } catch (err) { continue; } - res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); + res.setHeader('Cache-Control', `public, max-age=${maxAge}`); await util.promisify(res.sendFile.bind(res))(fn); return; } @@ -66,13 +66,13 @@ exports.expressPreSession = async (hookName, {app}) => { exports.expressCreateServer = (hookName, args, cb) => { // serve index.html under / args.app.get('/', (req, res) => { - res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req})); + res.send(required('ep_etherpad-lite/templates/index.html', {req})); }); // serve pad.html under /p args.app.get('/p/:pad', (req, res, next) => { // The below might break for pads being rewritten - const isReadOnly = !webaccess.userCanModify(req.params.pad, req); + const isReadOnly = !userCanModify(req.params.pad, req); hooks.callAll('padInitToolbar', { toolbar, @@ -81,7 +81,7 @@ exports.expressCreateServer = (hookName, args, cb) => { // can be removed when require-kernel is dropped res.header('Feature-Policy', 'sync-xhr \'self\''); - res.send(eejs.require('ep_etherpad-lite/templates/pad.html', { + res.send(required('ep_etherpad-lite/templates/pad.html', { req, toolbar, isReadOnly, @@ -94,7 +94,7 @@ exports.expressCreateServer = (hookName, args, cb) => { toolbar, }); - res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', { + res.send(required('ep_etherpad-lite/templates/timeslider.html', { req, toolbar, })); diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.ts similarity index 86% rename from src/node/hooks/express/static.js rename to src/node/hooks/express/static.ts index 26c18995a..962b845f5 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.ts @@ -19,8 +19,9 @@ const getTar = async () => { }; const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8'); const tar = {}; - for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) { - const files = relativeFiles.map(prefixLocalLibraryPath); + for (let [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) { + const relativeFilesMapped = relativeFiles as string[]; + const files = relativeFilesMapped.map(prefixLocalLibraryPath); tar[prefixLocalLibraryPath(key)] = files .concat(files.map((p) => p.replace(/\.js$/, ''))) .concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`)); @@ -28,7 +29,7 @@ const getTar = async () => { return tar; }; -exports.expressPreSession = async (hookName, {app}) => { +export const expressPreSession = async (hookName, {app}) => { // Cache both minified and static. const assetCache = new CachingMiddleware(); app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache)); @@ -62,8 +63,9 @@ exports.expressPreSession = async (hookName, {app}) => { const clientParts = plugins.parts.filter((part) => part.client_hooks != null); const clientPlugins = {}; for (const name of new Set(clientParts.map((part) => part.plugin))) { - clientPlugins[name] = {...plugins.plugins[name]}; - delete clientPlugins[name].package; + const mappedName = name as any + clientPlugins[mappedName] = {...plugins.plugins[mappedName]}; + delete clientPlugins[mappedName].package; } res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.ts similarity index 81% rename from src/node/hooks/express/tests.js rename to src/node/hooks/express/tests.ts index 66b47d2af..3b9db80b6 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.ts @@ -1,11 +1,14 @@ 'use strict'; -const path = require('path'); -const fsp = require('fs').promises; -const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const sanitizePathname = require('../../utils/sanitizePathname'); -const settings = require('../../utils/Settings'); +import path from 'path'; +import {promises as fsp} from "fs"; +import plugins from "../../../static/js/pluginfw/plugin_defs"; + +import sanitizePathname from "../../utils/sanitizePathname"; + +import {enableAdminUITests, root} from "../../utils/Settings"; +import {Presession} from "../../models/Presession"; // Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/' // instead of path.sep to separate pathname components. const findSpecs = async (specDir) => { @@ -29,16 +32,17 @@ const findSpecs = async (specDir) => { return specs; }; -exports.expressPreSession = async (hookName, {app}) => { +export const expressPreSession = async (hookName, {app}) => { app.get('/tests/frontend/frontendTestSpecs.json', (req, res, next) => { (async () => { const modules = []; await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => { - let {package: {path: pluginPath}} = def; + const mappedDef = def as Presession; + let {package: {path: pluginPath}} = mappedDef; if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep; const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`; for (const spec of await findSpecs(path.join(pluginPath, specDir))) { - if (plugin === 'ep_etherpad-lite' && !settings.enableAdminUITests && + if (plugin === 'ep_etherpad-lite' && !enableAdminUITests && spec.startsWith('admin')) continue; modules.push(`${plugin}/${specDir}/${spec.replace(/\.js$/, '')}`); } @@ -57,7 +61,7 @@ exports.expressPreSession = async (hookName, {app}) => { })().catch((err) => next(err || new Error(err))); }); - const rootTestFolder = path.join(settings.root, 'src/tests/frontend/'); + const rootTestFolder = path.join(root, 'src/tests/frontend/'); app.get('/tests/frontend/index.html', (req, res) => { res.redirect(['./', ...req.url.split('?').slice(1)].join('?')); diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.ts similarity index 85% rename from src/node/hooks/express/webaccess.js rename to src/node/hooks/express/webaccess.ts index 81ed69b07..58ecdace2 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.ts @@ -1,12 +1,17 @@ 'use strict'; -const assert = require('assert').strict; -const log4js = require('log4js'); -const httpLogger = log4js.getLogger('http'); -const settings = require('../../utils/Settings'); -const hooks = require('../../../static/js/pluginfw/hooks'); -const readOnlyManager = require('../../db/ReadOnlyManager'); +import {strict as assert} from "assert"; +import log4js from "log4js"; + +import {requireAuthentication, requireAuthorization, setUsers, users} from "../../utils/Settings"; + +import hooks from "../../../static/js/pluginfw/hooks"; + +import {getPadId, isReadOnlyId} from "../../db/ReadOnlyManager"; +import {UserIndexedModel} from "../../models/UserIndexedModel"; + +const httpLogger = log4js.getLogger('http'); hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead'; // Promisified wrapper around hooks.aCallFirst. @@ -17,7 +22,7 @@ const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, rej const aCallFirst0 = async (hookName, context, pred = null) => (await aCallFirst(hookName, context, pred))[0]; -exports.normalizeAuthzLevel = (level) => { +export const normalizeAuthzLevel = (level) => { if (!level) return false; switch (level) { case true: @@ -32,20 +37,20 @@ exports.normalizeAuthzLevel = (level) => { return false; }; -exports.userCanModify = (padId, req) => { - if (readOnlyManager.isReadOnlyId(padId)) return false; - if (!settings.requireAuthentication) return true; - const {session: {user} = {}} = req; +export const userCanModify = (padId, req) => { + if (isReadOnlyId(padId)) return false; + if (!requireAuthentication) return true; + const {session: {user} = {}}:SessionSocketModel = req; if (!user || user.readOnly) return false; assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization. - const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]); + const level = normalizeAuthzLevel(user.padAuthorizations[padId]); return level && level !== 'readOnly'; }; // Exported so that tests can set this to 0 to avoid unnecessary test slowness. -exports.authnFailureDelayMs = 1000; +export const authnFailureDelayMs = 1000; -const checkAccess = async (req, res, next) => { +export const checkAccess = async (req, res, next) => { const requireAdmin = req.path.toLowerCase().startsWith('/admin'); // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -88,16 +93,16 @@ const checkAccess = async (req, res, next) => { // authentication is checked and once after (if settings.requireAuthorization is true). const authorize = async () => { const grant = async (level) => { - level = exports.normalizeAuthzLevel(level); + level = normalizeAuthzLevel(level); if (!level) return false; const user = req.session.user; if (user == null) return true; // This will happen if authentication is not required. const encodedPadId = (req.path.match(/^\/p\/([^/]*)/) || [])[1]; if (encodedPadId == null) return true; let padId = decodeURIComponent(encodedPadId); - if (readOnlyManager.isReadOnlyId(padId)) { + if (isReadOnlyId(padId)) { // pad is read-only, first get the real pad ID - padId = await readOnlyManager.getPadId(padId); + padId = await getPadId(padId); if (padId == null) return false; } // The user was granted access to a pad. Remember the authorization level in the user's @@ -108,11 +113,11 @@ const checkAccess = async (req, res, next) => { }; const isAuthenticated = req.session && req.session.user; if (isAuthenticated && req.session.user.is_admin) return await grant('create'); - const requireAuthn = requireAdmin || settings.requireAuthentication; + const requireAuthn = requireAdmin || requireAuthentication; if (!requireAuthn) return await grant('create'); if (!isAuthenticated) return await grant(false); if (requireAdmin && !req.session.user.is_admin) return await grant(false); - if (!settings.requireAuthorization) return await grant('create'); + if (!requireAuthorization) return await grant('create'); return await grant(await aCallFirst0('authorize', {req, res, next, resource: req.path})); }; @@ -131,8 +136,10 @@ const checkAccess = async (req, res, next) => { // page). // /////////////////////////////////////////////////////////////////////////////////////////////// - if (settings.users == null) settings.users = {}; - const ctx = {req, res, users: settings.users, next}; + if (users == null) setUsers({}); + const ctx = {req, res, users: users, next, username: undefined, + password: undefined + }; // If the HTTP basic auth header is present, extract the username and password so it can be given // to authn plugins. const httpBasicAuth = req.headers.authorization && req.headers.authorization.startsWith('Basic '); @@ -148,7 +155,7 @@ const checkAccess = async (req, res, next) => { } if (!(await aCallFirst0('authenticate', ctx))) { // Fall back to HTTP basic auth. - const {[ctx.username]: {password} = {}} = settings.users; + const {[ctx.username]: {password} = {password: undefined}}:UserIndexedModel = users; if (!httpBasicAuth || !ctx.username || password == null || password !== ctx.password) { httpLogger.info(`Failed authentication from IP ${req.ip}`); if (await aCallFirst0('authnFailure', {req, res})) return; @@ -156,14 +163,14 @@ const checkAccess = async (req, res, next) => { // No plugin handled the authentication failure. Fall back to basic authentication. res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); // Delay the error response for 1s to slow down brute force attacks. - await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs)); + await new Promise((resolve) => setTimeout(resolve, authnFailureDelayMs)); res.status(401).send('Authentication Required'); return; } - settings.users[ctx.username].username = ctx.username; + users[ctx.username].username = ctx.username; // Make a shallow copy so that the password property can be deleted (to prevent it from // appearing in logs or in the database) without breaking future authentication attempts. - req.session.user = {...settings.users[ctx.username]}; + req.session.user = {...users[ctx.username]}; delete req.session.user.password; } if (req.session.user == null) { @@ -190,6 +197,7 @@ const checkAccess = async (req, res, next) => { * Express middleware to authenticate the user and check authorization. Must be installed after the * express-session middleware. */ -exports.checkAccess = (req, res, next) => { +// FIXME why same method twice? +export const checkAccess2 = (req, res, next) => { checkAccess(req, res, next).catch((err) => next(err || new Error(err))); }; diff --git a/src/node/hooks/i18n.js b/src/node/hooks/i18n.ts similarity index 79% rename from src/node/hooks/i18n.js rename to src/node/hooks/i18n.ts index c54348867..f6bf38f70 100644 --- a/src/node/hooks/i18n.js +++ b/src/node/hooks/i18n.ts @@ -1,12 +1,13 @@ 'use strict'; -const languages = require('languages4translatewiki'); -const fs = require('fs'); -const path = require('path'); -const _ = require('underscore'); -const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js'); -const existsSync = require('../utils/path_exists'); -const settings = require('../utils/Settings'); +import languages from 'languages4translatewiki'; +import fs from 'fs'; +import path from 'path'; +import _ from 'underscore'; +import pluginDefs from '../../static/js/pluginfw/plugin_defs.js'; +import {check} from '../utils/path_exists'; +import {customLocaleStrings, maxAge, root} from '../utils/Settings'; +import {Presession} from "../models/Presession"; // returns all existing messages merged together and grouped by langcode // {es: {"foo": "string"}, en:...} @@ -17,7 +18,7 @@ const getAllLocales = () => { // into `locales2paths` (files from various dirs are grouped by lang code) // (only json files with valid language code as name) const extractLangs = (dir) => { - if (!existsSync(dir)) return; + if (!check(dir)) return; let stat = fs.lstatSync(dir); if (!stat.isDirectory() || stat.isSymbolicLink()) return; @@ -37,13 +38,14 @@ const getAllLocales = () => { }; // add core supported languages first - extractLangs(path.join(settings.root, 'src/locales')); + extractLangs(path.join(root, 'src/locales')); // add plugins languages (if any) - for (const {package: {path: pluginPath}} of Object.values(pluginDefs.plugins)) { + for (const val of Object.values(pluginDefs.plugins)) { + const pluginPath:Presession = val as Presession // plugin locales should overwrite etherpad's core locales - if (pluginPath.endsWith('/ep_etherpad-lite') === true) continue; - extractLangs(path.join(pluginPath, 'locales')); + if (pluginPath.package.path.endsWith('/ep_etherpad-lite') === true) continue; + extractLangs(path.join(pluginPath.package.path, 'locales')); } // Build a locale index (merge all locale data other than user-supplied overrides) @@ -68,9 +70,9 @@ const getAllLocales = () => { const wrongFormatErr = Error( 'customLocaleStrings in wrong format. See documentation ' + 'for Customization for Administrators, under Localization.'); - if (settings.customLocaleStrings) { - if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr; - _.each(settings.customLocaleStrings, (overrides, langcode) => { + if (customLocaleStrings) { + if (typeof customLocaleStrings !== 'object') throw wrongFormatErr; + _.each(customLocaleStrings, (overrides, langcode) => { if (typeof overrides !== 'object') throw wrongFormatErr; _.each(overrides, (localeString, key) => { if (typeof localeString !== 'string') throw wrongFormatErr; @@ -112,7 +114,7 @@ exports.expressPreSession = async (hookName, {app}) => { // works with /locale/en and /locale/en.json requests const locale = req.params.locale.split('.')[0]; if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) { - res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); + res.setHeader('Cache-Control', `public, max-age=${maxAge}`); res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`); } else { @@ -121,7 +123,7 @@ exports.expressPreSession = async (hookName, {app}) => { }); app.get('/locales.json', (req, res) => { - res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); + res.setHeader('Cache-Control', `public, max-age=${maxAge}`); res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.send(localeIndex); }); diff --git a/src/node/models/CMDOptions.ts b/src/node/models/CMDOptions.ts new file mode 100644 index 000000000..925170e1d --- /dev/null +++ b/src/node/models/CMDOptions.ts @@ -0,0 +1,11 @@ +export type CMDOptions = { + cwd?: string, + stdio?: string|any[], + env?: NodeJS.ProcessEnv +} + +export type CMDPromise = { + stdout: string, + stderr: string, + child: any +} diff --git a/src/node/models/LogLevel.ts b/src/node/models/LogLevel.ts new file mode 100644 index 000000000..bd007c23f --- /dev/null +++ b/src/node/models/LogLevel.ts @@ -0,0 +1 @@ +export type LogLevel = "DEBUG"|"INFO"|"WARN"|"ERROR" diff --git a/src/node/models/PadDiffModel.ts b/src/node/models/PadDiffModel.ts new file mode 100644 index 000000000..c201b1fb4 --- /dev/null +++ b/src/node/models/PadDiffModel.ts @@ -0,0 +1,3 @@ +export type PadDiffModel = { + _authors: any[] +} diff --git a/src/node/models/Presession.ts b/src/node/models/Presession.ts new file mode 100644 index 000000000..bd8146bc3 --- /dev/null +++ b/src/node/models/Presession.ts @@ -0,0 +1,5 @@ +export type Presession = { + package:{ + path: string, + } +} diff --git a/src/node/models/SessionSocketModel.ts b/src/node/models/SessionSocketModel.ts index d2713d11d..ff0c730aa 100644 --- a/src/node/models/SessionSocketModel.ts +++ b/src/node/models/SessionSocketModel.ts @@ -3,6 +3,8 @@ type SessionSocketModel = { user?: { username?: string, is_admin?: boolean + readOnly?: boolean, + padAuthorizations?: any } } } diff --git a/src/node/models/UserIndexedModel.ts b/src/node/models/UserIndexedModel.ts new file mode 100644 index 000000000..0c808a41b --- /dev/null +++ b/src/node/models/UserIndexedModel.ts @@ -0,0 +1,5 @@ +export type UserIndexedModel = { + [key: string]: { + password?: string|undefined, + } +} diff --git a/src/node/server.ts b/src/node/server.ts index cc23c7b35..2797c8d74 100644 --- a/src/node/server.ts +++ b/src/node/server.ts @@ -203,11 +203,13 @@ export const stop = async () => { } catch (err) { logger.error('Error occurred while stopping Etherpad'); state = State.STATE_TRANSITION_FAILED; + // @ts-ignore stopDoneGate.resolve(); return await exit(err); } logger.info('Etherpad stopped'); state = State.STOPPED; + // @ts-ignore stopDoneGate.resolve(); }; diff --git a/src/node/utils/Abiword.js b/src/node/utils/Abiword.ts similarity index 80% rename from src/node/utils/Abiword.js rename to src/node/utils/Abiword.ts index 1ed487ae1..b83fc6a3d 100644 --- a/src/node/utils/Abiword.js +++ b/src/node/utils/Abiword.ts @@ -19,20 +19,21 @@ * limitations under the License. */ -const spawn = require('child_process').spawn; -const async = require('async'); -const settings = require('./Settings'); -const os = require('os'); +import {spawn} from "child_process"; + +import async from 'async'; +import {abiword} from './Settings'; +import os from 'os'; // on windows we have to spawn a process for each convertion, // cause the plugin abicommand doesn't exist on this platform if (os.type().indexOf('Windows') > -1) { exports.convertFile = async (srcFile, destFile, type) => { - const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]); + const abiword2 = spawn(abiword, [`--to=${destFile}`, srcFile]); let stdoutBuffer = ''; - abiword.stdout.on('data', (data) => { stdoutBuffer += data.toString(); }); - abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); - await new Promise((resolve, reject) => { + abiword2.stdout.on('data', (data) => { stdoutBuffer += data.toString(); }); + abiword2.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); + await new Promise((resolve, reject) => { abiword.on('exit', (code) => { if (code !== 0) return reject(new Error(`Abiword died with exit code ${code}`)); if (stdoutBuffer !== '') { @@ -46,14 +47,14 @@ if (os.type().indexOf('Windows') > -1) { // communicate with it via stdin/stdout // thats much faster, about factor 10 } else { - let abiword; + let abiword2; let stdoutCallback = null; const spawnAbiword = () => { - abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']); + abiword2 = spawn(abiword, ['--plugin', 'AbiCommand']); let stdoutBuffer = ''; let firstPrompt = true; - abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); - abiword.on('exit', (code) => { + abiword2.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); + abiword2.on('exit', (code) => { spawnAbiword(); if (stdoutCallback != null) { stdoutCallback(new Error(`Abiword died with exit code ${code}`)); diff --git a/src/node/utils/AbsolutePaths.js b/src/node/utils/AbsolutePaths.ts similarity index 94% rename from src/node/utils/AbsolutePaths.js rename to src/node/utils/AbsolutePaths.ts index 73a96bb67..5356c5564 100644 --- a/src/node/utils/AbsolutePaths.js +++ b/src/node/utils/AbsolutePaths.ts @@ -19,9 +19,10 @@ * limitations under the License. */ -const log4js = require('log4js'); -const path = require('path'); -const _ = require('underscore'); +import log4js from 'log4js'; +import path from "path"; + +import _ from "underscore"; const absPathLogger = log4js.getLogger('AbsolutePaths'); @@ -75,7 +76,7 @@ const popIfEndsWith = (stringArray, lastDesiredElements) => { * @return {string} The identified absolute base path. If such path cannot be * identified, prints a log and exits the application. */ -exports.findEtherpadRoot = () => { +export const findEtherpadRoot = () => { if (etherpadRoot != null) { return etherpadRoot; } @@ -131,12 +132,12 @@ exports.findEtherpadRoot = () => { * it is returned unchanged. Otherwise it is interpreted * relative to exports.root. */ -exports.makeAbsolute = (somePath) => { +export const makeAbsolute = (somePath) => { if (path.isAbsolute(somePath)) { return somePath; } - const rewrittenPath = path.join(exports.findEtherpadRoot(), somePath); + const rewrittenPath = path.join(findEtherpadRoot(), somePath); absPathLogger.debug(`Relative path "${somePath}" can be rewritten to "${rewrittenPath}"`); return rewrittenPath; @@ -150,7 +151,7 @@ exports.makeAbsolute = (somePath) => { * a subdirectory of the base one * @return {boolean} */ -exports.isSubdir = (parent, arbitraryDir) => { +export const isSubdir = (parent, arbitraryDir) => { // modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825 const relative = path.relative(parent, arbitraryDir); const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); diff --git a/src/node/utils/Cli.js b/src/node/utils/Cli.ts similarity index 96% rename from src/node/utils/Cli.js rename to src/node/utils/Cli.ts index a5cdee83a..946fd66ca 100644 --- a/src/node/utils/Cli.js +++ b/src/node/utils/Cli.ts @@ -21,9 +21,8 @@ */ // An object containing the parsed command-line options -exports.argv = {}; +export const argv = process.argv.slice(2); -const argv = process.argv.slice(2); let arg, prevArg; // Loop through args diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.ts similarity index 84% rename from src/node/utils/ExportEtherpad.js rename to src/node/utils/ExportEtherpad.ts index e20739ad3..d548590bd 100644 --- a/src/node/utils/ExportEtherpad.js +++ b/src/node/utils/ExportEtherpad.ts @@ -14,17 +14,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import {Stream} from "./Stream"; -const Stream = require('./Stream'); -const assert = require('assert').strict; -const authorManager = require('../db/AuthorManager'); -const hooks = require('../../static/js/pluginfw/hooks'); -const padManager = require('../db/PadManager'); +import {strict as assert} from "assert"; -exports.getPadRaw = async (padId, readOnlyId) => { +import {getAuthor} from "../db/AuthorManager"; + +import hooks from "../../static/js/pluginfw/hooks"; + +import {getPad} from "../db/PadManager"; + +export const getPadRaw = async (padId, readOnlyId) => { const dstPfx = `pad:${readOnlyId || padId}`; const [pad, customPrefixes] = await Promise.all([ - padManager.getPad(padId), + getPad(padId), hooks.aCallAll('exportEtherpadAdditionalContent'), ]); const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix) => { @@ -43,7 +46,7 @@ exports.getPadRaw = async (padId, readOnlyId) => { const records = (function* () { for (const authorId of pad.getAllAuthors()) { yield [`globalAuthor:${authorId}`, (async () => { - const authorEntry = await authorManager.getAuthor(authorId); + const authorEntry = await getAuthor(authorId); if (!authorEntry) return undefined; // Becomes unset when converted to JSON. if (authorEntry.padIDs) authorEntry.padIDs = readOnlyId || padId; return authorEntry; diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.ts similarity index 85% rename from src/node/utils/ExportHelper.js rename to src/node/utils/ExportHelper.ts index 7962476e8..b4a5765ee 100644 --- a/src/node/utils/ExportHelper.js +++ b/src/node/utils/ExportHelper.ts @@ -19,11 +19,10 @@ * limitations under the License. */ -const AttributeMap = require('../../static/js/AttributeMap'); -const Changeset = require('../../static/js/Changeset'); +import AttributeMap from '../../static/js/AttributeMap'; +import Changeset from '../../static/js/Changeset'; -exports.getPadPlainText = (pad, revNum) => { - const _analyzeLine = exports._analyzeLine; +export const getPadPlainText = (pad, revNum) => { const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext); const textLines = atext.text.slice(0, -1).split('\n'); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); @@ -45,8 +44,14 @@ exports.getPadPlainText = (pad, revNum) => { }; -exports._analyzeLine = (text, aline, apool) => { - const line = {}; +export const _analyzeLine = (text, aline, apool) => { + const line = { + listLevel: undefined, + text: undefined, + listTypeName: undefined, + aline: undefined, + start: undefined + }; // identify list let lineMarker = 0; @@ -81,5 +86,5 @@ exports._analyzeLine = (text, aline, apool) => { }; -exports._encodeWhitespace = +export const _encodeWhitespace = (s) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`); diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.ts similarity index 95% rename from src/node/utils/ExportHtml.js rename to src/node/utils/ExportHtml.ts index d14f40e6e..c8a7d6884 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.ts @@ -15,16 +15,19 @@ * limitations under the License. */ -const Changeset = require('../../static/js/Changeset'); -const attributes = require('../../static/js/attributes'); -const padManager = require('../db/PadManager'); -const _ = require('underscore'); -const Security = require('../../static/js/security'); -const hooks = require('../../static/js/pluginfw/hooks'); -const eejs = require('../eejs'); -const _analyzeLine = require('./ExportHelper')._analyzeLine; -const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; -const padutils = require('../../static/js/pad_utils').padutils; +import Changeset from '../../static/js/Changeset'; +import attributes from "../../static/js/attributes"; + +import {getPad} from "../db/PadManager"; + +import _ from "underscore"; + +import Security from '../../static/js/security'; +import hooks from '../../static/js/pluginfw/hooks'; +import {required} from '../eejs'; +import {_analyzeLine, _encodeWhitespace} from "./ExportHelper"; + +import {padutils} from "../../static/js/pad_utils"; const getPadHTML = async (pad, revNum) => { let atext = pad.atext; @@ -38,7 +41,7 @@ const getPadHTML = async (pad, revNum) => { return await getHTMLFromAtext(pad, atext); }; -const getHTMLFromAtext = async (pad, atext, authorColors) => { +export const getHTMLFromAtext = async (pad, atext, authorColors?) => { const apool = pad.apool(); const textLines = atext.text.slice(0, -1).split('\n'); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); @@ -456,8 +459,8 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { return pieces.join(''); }; -exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => { - const pad = await padManager.getPad(padId); +export const getPadHTMLDocument = async (padId, revNum, readOnlyId?) => { + const pad = await getPad(padId); // Include some Styles into the Head for Export let stylesForExportCSS = ''; @@ -472,7 +475,7 @@ exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => { html += hookHtml; } - return eejs.require('ep_etherpad-lite/templates/export_html.html', { + return required('ep_etherpad-lite/templates/export_html.html', { body: html, padId: Security.escapeHTML(readOnlyId || padId), extraCSS: stylesForExportCSS, @@ -525,7 +528,4 @@ const _processSpaces = (s) => { } } return parts.join(''); -}; - -exports.getPadHTML = getPadHTML; -exports.getHTMLFromAtext = getHTMLFromAtext; +} diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.ts similarity index 97% rename from src/node/utils/ExportTxt.js rename to src/node/utils/ExportTxt.ts index 9511dd0e7..7915725fa 100644 --- a/src/node/utils/ExportTxt.js +++ b/src/node/utils/ExportTxt.ts @@ -39,7 +39,7 @@ const getPadTXT = async (pad, revNum) => { // This is different than the functionality provided in ExportHtml as it provides formatting // functionality that is designed specifically for TXT exports -const getTXTFromAtext = (pad, atext, authorColors) => { +export const getTXTFromAtext = (pad, atext, authorColors?) => { const apool = pad.apool(); const textLines = atext.text.slice(0, -1).split('\n'); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); @@ -56,7 +56,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => { }); const getLineTXT = (text, attribs) => { - const propVals = [false, false, false]; + const propVals:(boolean|number)[] = [false, false, false]; const ENTER = 1; const STAY = 2; const LEAVE = 0; @@ -256,9 +256,8 @@ const getTXTFromAtext = (pad, atext, authorColors) => { return pieces.join(''); }; -exports.getTXTFromAtext = getTXTFromAtext; -exports.getPadTXTDocument = async (padId, revNum) => { +export const getPadTXTDocument = async (padId, revNum) => { const pad = await padManager.getPad(padId); return getPadTXT(pad, revNum); }; diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.ts similarity index 86% rename from src/node/utils/ImportEtherpad.js rename to src/node/utils/ImportEtherpad.ts index da7e750ff..09dca8ecd 100644 --- a/src/node/utils/ImportEtherpad.js +++ b/src/node/utils/ImportEtherpad.ts @@ -16,19 +16,21 @@ * limitations under the License. */ -const AttributePool = require('../../static/js/AttributePool'); -const {Pad} = require('../db/Pad'); -const Stream = require('./Stream'); -const authorManager = require('../db/AuthorManager'); -const db = require('../db/DB'); -const hooks = require('../../static/js/pluginfw/hooks'); -const log4js = require('log4js'); -const supportedElems = require('../../static/js/contentcollector').supportedElems; -const ueberdb = require('ueberdb2'); +import {AttributePool} from '../../static/js/AttributePool'; +import {Pad} from '../db/Pad'; +import {Stream} from './Stream'; +import {addPad, doesAuthorExist} from '../db/AuthorManager'; +import {db} from '../db/DB'; +import hooks from '../../static/js/pluginfw/hooks'; +import log4js from "log4js"; + +import {supportedElems} from "../../static/js/contentcollector"; + +import ueberdb from 'ueberdb2'; const logger = log4js.getLogger('ImportEtherpad'); -exports.setPadRaw = async (padId, r, authorId = '') => { +export const setPadRaw = async (padId, r, authorId = '') => { const records = JSON.parse(r); // get supported block Elements from plugins, we will use this later. @@ -69,7 +71,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => { throw new TypeError('globalAuthor padIDs subkey is not a string'); } checkOriginalPadId(value.padIDs); - if (await authorManager.doesAuthorExist(id)) { + if (await doesAuthorExist(id)) { existingAuthors.add(id); return; } @@ -115,7 +117,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => { const writeOps = (function* () { for (const [k, v] of data) yield db.set(k, v); - for (const a of existingAuthors) yield authorManager.addPad(a, padId); + for (const a of existingAuthors) yield addPad(a as string, padId); })(); for (const op of new Stream(writeOps).batch(100).buffer(99)) await op; }; diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.ts similarity index 92% rename from src/node/utils/ImportHtml.js rename to src/node/utils/ImportHtml.ts index d7b2172b0..2e5de910a 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.ts @@ -15,15 +15,15 @@ * limitations under the License. */ -const log4js = require('log4js'); -const Changeset = require('../../static/js/Changeset'); -const contentcollector = require('../../static/js/contentcollector'); -const jsdom = require('jsdom'); +import log4js from 'log4js'; +import Changeset from '../../static/js/Changeset'; +import contentcollector from '../../static/js/contentcollector'; +import jsdom from 'jsdom'; const apiLogger = log4js.getLogger('ImportHtml'); let processor; -exports.setPadHTML = async (pad, html, authorId = '') => { +export const setPadHTML = async (pad, html, authorId = '') => { if (processor == null) { const [{rehype}, {default: minifyWhitespace}] = await Promise.all([import('rehype'), import('rehype-minify-whitespace')]); @@ -90,4 +90,4 @@ exports.setPadHTML = async (pad, html, authorId = '') => { apiLogger.debug(`The changeset: ${theChangeset}`); await pad.setText('\n', authorId); await pad.appendRevision(theChangeset, authorId); -}; +} diff --git a/src/node/utils/LibreOffice.js b/src/node/utils/LibreOffice.ts similarity index 91% rename from src/node/utils/LibreOffice.js rename to src/node/utils/LibreOffice.ts index 339209194..dddaf071d 100644 --- a/src/node/utils/LibreOffice.js +++ b/src/node/utils/LibreOffice.ts @@ -17,20 +17,23 @@ * limitations under the License. */ -const async = require('async'); -const fs = require('fs').promises; -const log4js = require('log4js'); -const os = require('os'); -const path = require('path'); -const runCmd = require('./run_cmd'); -const settings = require('./Settings'); +import async from 'async'; +import log4js from 'log4js'; +import {promises as fs} from "fs"; + +import os from 'os'; +import path from "path"; + +import {exportCMD} from "./run_cmd"; + +import {soffice} from "./Settings"; const logger = log4js.getLogger('LibreOffice'); const doConvertTask = async (task) => { const tmpDir = os.tmpdir(); - const p = runCmd([ - settings.soffice, + const p = await exportCMD([ + soffice, '--headless', '--invisible', '--nologo', @@ -81,7 +84,7 @@ const queue = async.queue(doConvertTask, 1); * @param {String} type The type to convert into * @param {Function} callback Standard callback function */ -exports.convertFile = async (srcFile, destFile, type) => { +export const convertFile = async (srcFile, destFile, type) => { // Used for the moving of the file, not the conversion const fileExtension = type; diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.ts similarity index 91% rename from src/node/utils/Minify.js rename to src/node/utils/Minify.ts index 2e8a2d960..03a7a403e 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.ts @@ -21,21 +21,28 @@ * limitations under the License. */ -const settings = require('./Settings'); -const fs = require('fs').promises; -const path = require('path'); -const plugins = require('../../static/js/pluginfw/plugin_defs'); -const RequireKernel = require('etherpad-require-kernel'); -const mime = require('mime-types'); -const Threads = require('threads'); -const log4js = require('log4js'); +import {maxAge, root} from './Settings'; +import {promises as fs} from "fs"; + +import path from "path"; + +import plugins from "../../static/js/pluginfw/plugin_defs"; + +import RequireKernel from "etherpad-require-kernel"; + +import mime from "mime-types"; + +import Threads from "threads"; + +import log4js from "log4js"; + const sanitizePathname = require('./sanitizePathname'); const logger = log4js.getLogger('Minify'); -const ROOT_DIR = path.join(settings.root, 'src/static/'); +const ROOT_DIR = path.join(root, 'src/static/'); -const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2); +const threadsPool = Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2); const LIBRARY_WHITELIST = [ 'async', @@ -89,7 +96,7 @@ const requestURI = async (url, method, headers) => { return await p; }; -const requestURIs = (locations, method, headers, callback) => { +export const requestURIs = (locations, method, headers, callback) => { Promise.all(locations.map(async (loc) => { try { return await requestURI(loc, method, headers); @@ -176,10 +183,10 @@ const minify = async (req, res) => { date.setMilliseconds(0); res.setHeader('last-modified', date.toUTCString()); res.setHeader('date', (new Date()).toUTCString()); - if (settings.maxAge !== undefined) { - const expiresDate = new Date(Date.now() + settings.maxAge * 1000); + if (maxAge !== undefined) { + const expiresDate = new Date(Date.now() + maxAge * 1000); res.setHeader('expires', expiresDate.toUTCString()); - res.setHeader('cache-control', `max-age=${settings.maxAge}`); + res.setHeader('cache-control', `max-age=${maxAge}`); } } @@ -271,7 +278,7 @@ const requireDefinition = () => `var require = ${RequireKernel.kernelSource};\n` const getFileCompressed = async (filename, contentType) => { let content = await getFile(filename); - if (!content || !settings.minify) { + if (!content || !minify) { return content; } else if (contentType === 'application/javascript') { return await new Promise((resolve) => { @@ -317,10 +324,8 @@ const getFile = async (filename) => { return await fs.readFile(path.resolve(ROOT_DIR, filename)); }; -exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err))); +export const minifyExp = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err))); -exports.requestURIs = requestURIs; - -exports.shutdown = async (hookName, context) => { +export const shutdown = async (hookName, context) => { await threadsPool.terminate(); }; diff --git a/src/node/utils/MinifyWorker.js b/src/node/utils/MinifyWorker.ts similarity index 80% rename from src/node/utils/MinifyWorker.js rename to src/node/utils/MinifyWorker.ts index 364ecc96c..f5307cc9a 100644 --- a/src/node/utils/MinifyWorker.js +++ b/src/node/utils/MinifyWorker.ts @@ -3,11 +3,14 @@ * Worker thread to minify JS & CSS files out of the main NodeJS thread */ -const CleanCSS = require('clean-css'); -const Terser = require('terser'); -const fsp = require('fs').promises; -const path = require('path'); -const Threads = require('threads'); +import CleanCSS from 'clean-css'; +import Terser from "terser"; + +import {promises as fsp} from "fs"; + +import path from "path"; + +import Threads from "threads"; const compressJS = (content) => Terser.minify(content); diff --git a/src/node/utils/Settings.ts b/src/node/utils/Settings.ts index 0d5f1e53c..2813e5030 100644 --- a/src/node/utils/Settings.ts +++ b/src/node/utils/Settings.ts @@ -27,6 +27,8 @@ * limitations under the License. */ +import exp from "constants"; + const absolutePaths = require('./AbsolutePaths'); const deepEqual = require('fast-deep-equal/es6'); import fs from 'fs'; @@ -35,6 +37,7 @@ import path from 'path'; const argv = require('./Cli').argv; import jsonminify from 'jsonminify'; import log4js from 'log4js'; +import {LogLevel} from "../models/LogLevel"; const randomString = require('./randomstring'); const suppressDisableMsg = ' -- To suppress these warning messages change ' + 'suppressErrorsInPadText to true in your settings.json\n'; @@ -70,7 +73,7 @@ initLogging(defaultLogLevel, defaultLogConfig()); /* Root path of the installation */ export const root = absolutePaths.findEtherpadRoot(); logger.info('All relative paths will be interpreted relative to the identified ' + - `Etherpad base dir: ${exports.root}`); + `Etherpad base dir: ${root}`); export const settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json'); export const credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'); @@ -93,14 +96,14 @@ export const favicon = null; * Initialized to null, so we can spot an old configuration file and invite the * user to update it before falling back to the default. */ -export const skinName = null; +export let skinName = null; export const skinVariants = 'super-light-toolbar super-light-editor light-background'; /** * The IP ep-lite should listen to */ -export const ip = '0.0.0.0'; +export const ip:String = '0.0.0.0'; /** * The Port ep-lite should listen to @@ -118,6 +121,12 @@ export const suppressErrorsInPadText = false; */ export const ssl = false; +export const sslKeys = { + cert: undefined, + key: undefined, + ca: undefined, +} + /** * socket.io transport methods **/ @@ -142,12 +151,12 @@ export const dbType = 'dirty'; /** * This setting is passed with dbType to ueberDB to set up the database */ -export const dbSettings = {filename: path.join(exports.root, 'var/dirty.db')}; +export const dbSettings = {filename: path.join(root, 'var/dirty.db')}; /** * The default Text of a new pad */ -export const defaultPadText = [ +export let defaultPadText = [ 'Welcome to Etherpad!', '', 'This pad text is synchronized as you type, so that everyone viewing this page sees the same ' + @@ -244,12 +253,12 @@ export const minify = true; /** * The path of the abiword executable */ -export const abiword = null; +export let abiword = null; /** * The path of the libreoffice executable */ -export const soffice = null; +export let soffice = null; /** * The path of the tidy executable @@ -264,7 +273,7 @@ export const allowUnknownFileEnds = true; /** * The log level of log4js */ -export const loglevel = defaultLogLevel; +export const loglevel:LogLevel = defaultLogLevel; /** * Disable IP logging @@ -299,7 +308,7 @@ export const logconfig = defaultLogConfig(); /* * Session Key, do not sure this. */ -export const sessionKey = false; +export let sessionKey: string|boolean = false; /* * Trust Proxy, whether or not trust the x-forwarded-for header. @@ -333,7 +342,7 @@ export const cookie = { */ export const requireAuthentication = false; export const requireAuthorization = false; -export const users = {}; +export let users = {}; /* * Show settings in admin page, by default it is true @@ -384,6 +393,10 @@ export const exposeVersion = false; */ export const customLocaleStrings = {}; +export const setUsers = (newUsers:any) => { + users = newUsers; +} + /* * From Etherpad 1.8.3 onwards, import and export of pads is always rate * limited. @@ -434,7 +447,7 @@ export const enableAdminUITests = false; // checks if abiword is avaiable export const abiwordAvailable = () => { - if (exports.abiword != null) { + if (abiword != null) { return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; } else { return 'no'; @@ -442,7 +455,7 @@ export const abiwordAvailable = () => { }; export const sofficeAvailable = () => { - if (exports.soffice != null) { + if (soffice != null) { return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; } else { return 'no'; @@ -450,7 +463,7 @@ export const sofficeAvailable = () => { }; export const exportAvailable = () => { - const abiword = exports.abiwordAvailable(); + const abiword = abiwordAvailable(); const soffice = sofficeAvailable(); if (abiword === 'no' && soffice === 'no') { @@ -467,7 +480,7 @@ export const exportAvailable = () => { export const getGitCommit = () => { let version = ''; try { - let rootPath = exports.root; + let rootPath = root; if (fs.lstatSync(`${rootPath}/.git`).isFile()) { rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8'); rootPath = rootPath.split(' ').pop().trim(); @@ -735,88 +748,90 @@ export const reloadSettings = () => { storeSettings(settings); storeSettings(credentials); - initLogging(exports.loglevel, exports.logconfig); + initLogging(loglevel, logconfig); - if (!exports.skinName) { + if (!skinName) { logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' + 'update your settings.json. Falling back to the default "colibris".'); - exports.skinName = 'colibris'; + skinName = 'colibris'; } // checks if skinName has an acceptable value, otherwise falls back to "colibris" - if (exports.skinName) { - const skinBasePath = path.join(exports.root, 'src', 'static', 'skins'); - const countPieces = exports.skinName.split(path.sep).length; + if (skinName) { + const skinBasePath = path.join(root, 'src', 'static', 'skins'); + const countPieces = skinName.split(path.sep).length; if (countPieces !== 1) { logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` + - `not valid: "${exports.skinName}". Falling back to the default "colibris".`); + `not valid: "${skinName}". Falling back to the default "colibris".`); - exports.skinName = 'colibris'; + skinName = 'colibris'; } // informative variable, just for the log messages - let skinPath = path.join(skinBasePath, exports.skinName); + let skinPath = path.join(skinBasePath, skinName); // what if someone sets skinName == ".." or "."? We catch him! if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) { logger.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` + 'Falling back to the default "colibris".'); - exports.skinName = 'colibris'; - skinPath = path.join(skinBasePath, exports.skinName); + skinName = 'colibris'; + skinPath = path.join(skinBasePath, skinName); } if (fs.existsSync(skinPath) === false) { logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); - exports.skinName = 'colibris'; - skinPath = path.join(skinBasePath, exports.skinName); + skinName = 'colibris'; + skinPath = path.join(skinBasePath, skinName); } - logger.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`); + logger.info(`Using skin "${skinName}" in dir: ${skinPath}`); } - if (exports.abiword) { + if (abiword) { // Check abiword actually exists - if (exports.abiword != null) { - fs.exists(exports.abiword, (exists: boolean) => { + if (abiword != null) { + fs.exists(abiword, (exists: boolean) => { if (!exists) { const abiwordError = 'Abiword does not exist at this path, check your settings file.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`; + if (!suppressErrorsInPadText) { + defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`; } - logger.error(`${abiwordError} File location: ${exports.abiword}`); - exports.abiword = null; + logger.error(`${abiwordError} File location: ${abiword}`); + abiword = null; } }); } } - if (exports.soffice) { - fs.exists(exports.soffice, (exists: boolean) => { + if (soffice) { + fs.exists(soffice, (exists: boolean) => { if (!exists) { const sofficeError = 'soffice (libreoffice) does not exist at this path, check your settings file.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`; + if (!suppressErrorsInPadText) { + defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`; } - logger.error(`${sofficeError} File location: ${exports.soffice}`); - exports.soffice = null; + logger.error(`${sofficeError} File location: ${soffice}`); + soffice = null; } }); } - if (!exports.sessionKey) { + if (!sessionKey) { const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); try { - exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); + sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); logger.info(`Session key loaded from: ${sessionkeyFilename}`); } catch (e) { logger.info( `Session key file "${sessionkeyFilename}" not found. Creating with random contents.`); - exports.sessionKey = randomString(32); - fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8'); + sessionKey = randomString(32); + // FIXME Check out why this can be string boolean or Array + // @ts-ignore + fs.writeFileSync(sessionkeyFilename, sessionKey, 'utf8'); } } else { logger.warn('Declaring the sessionKey in the settings.json is deprecated. ' + @@ -825,17 +840,17 @@ export const reloadSettings = () => { 'Interface then you can ignore this message.'); } - if (exports.dbType === 'dirty') { + if (dbType === 'dirty') { const dirtyWarning = 'DirtyDB is used. This is not recommended for production.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`; + if (!suppressErrorsInPadText) { + defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`; } - exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename); - logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`); + dbSettings.filename = absolutePaths.makeAbsolute(dbSettings.filename); + logger.warn(`${dirtyWarning} File location: ${dbSettings.filename}`); } - if (exports.ip === '') { + if (ip === '') { // using Unix socket for connectivity logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' + '"port" parameter will be interpreted as the path to a Unix socket to bind at.'); @@ -852,7 +867,7 @@ export const reloadSettings = () => { * ACHTUNG: this may prevent caching HTTP proxies to work * TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead */ - logger.info(`Random string used for versioning assets: ${exports.randomVersionString}`); + logger.info(`Random string used for versioning assets: ${randomVersionString}`); }; exports.exportedForTestingOnly = { @@ -860,6 +875,6 @@ exports.exportedForTestingOnly = { }; // initially load settings -exports.reloadSettings(); +reloadSettings(); diff --git a/src/node/utils/Stream.js b/src/node/utils/Stream.ts similarity index 98% rename from src/node/utils/Stream.js rename to src/node/utils/Stream.ts index 611b83b33..0a083af84 100644 --- a/src/node/utils/Stream.js +++ b/src/node/utils/Stream.ts @@ -4,7 +4,9 @@ * Wrapper around any iterable that adds convenience methods that standard JavaScript iterable * objects lack. */ -class Stream { +export class Stream { + private readonly _iter: any; + private _next: null; /** * @returns {Stream} A Stream that yields values in the half-open range [start, end). */ @@ -130,5 +132,3 @@ class Stream { */ [Symbol.iterator]() { return this._iter; } } - -module.exports = Stream; diff --git a/src/node/utils/TidyHtml.js b/src/node/utils/TidyHtml.ts similarity index 83% rename from src/node/utils/TidyHtml.js rename to src/node/utils/TidyHtml.ts index 5b48cdbad..cbedc8056 100644 --- a/src/node/utils/TidyHtml.js +++ b/src/node/utils/TidyHtml.ts @@ -3,16 +3,17 @@ * Tidy up the HTML in a given file */ -const log4js = require('log4js'); -const settings = require('./Settings'); -const spawn = require('child_process').spawn; +import log4js from 'log4js'; +import {tidyHtml} from "./Settings"; + +import {spawn} from "child_process"; exports.tidy = (srcFile) => { const logger = log4js.getLogger('TidyHtml'); return new Promise((resolve, reject) => { // Don't do anything if Tidy hasn't been enabled - if (!settings.tidyHtml) { + if (!tidyHtml) { logger.debug('tidyHtml has not been configured yet, ignoring tidy request'); return resolve(null); } @@ -21,7 +22,7 @@ exports.tidy = (srcFile) => { // Spawn a new tidy instance that cleans up the file inline logger.debug(`Tidying ${srcFile}`); - const tidy = spawn(settings.tidyHtml, ['-modify', srcFile]); + const tidy = spawn(tidyHtml, ['-modify', srcFile]); // Keep track of any error messages tidy.stderr.on('data', (data) => { diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.ts similarity index 90% rename from src/node/utils/caching_middleware.js rename to src/node/utils/caching_middleware.ts index 3cc4daf27..93512e1ee 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.ts @@ -16,15 +16,20 @@ * limitations under the License. */ -const Buffer = require('buffer').Buffer; -const fs = require('fs'); -const fsp = fs.promises; -const path = require('path'); -const zlib = require('zlib'); -const settings = require('./Settings'); -const existsSync = require('./path_exists'); -const util = require('util'); +import {Buffer} from "buffer"; +import fs, {Stats} from "fs"; + +import path from "path"; + +import {check} from './path_exists' +import zlib from "zlib"; + +import {root} from "./Settings"; + +import util from "util"; + +const fsp = fs.promises; /* * The crypto module can be absent on reduced node installations. * @@ -45,8 +50,8 @@ try { _crypto = undefined; } -let CACHE_DIR = path.join(settings.root, 'var/'); -CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined; +let CACHE_DIR = path.join(root, 'var/'); +CACHE_DIR = check(CACHE_DIR) ? CACHE_DIR : undefined; const responseCache = {}; @@ -78,7 +83,7 @@ if (_crypto) { should replace this. */ -module.exports = class CachingMiddleware { +export class CachingMiddleware { handle(req, res, next) { this._handle(req, res, next).catch((err) => next(err || new Error(err))); } @@ -88,8 +93,15 @@ module.exports = class CachingMiddleware { return next(undefined, req, res); } - const oldReq = {}; - const oldRes = {}; + const oldReq = { + method: undefined + }; + const oldRes = { + write: undefined, + end: undefined, + setHeader: undefined, + writeHead: undefined + }; const supportsGzip = (req.get('Accept-Encoding') || '').indexOf('gzip') !== -1; @@ -100,7 +112,7 @@ module.exports = class CachingMiddleware { const stats = await fsp.stat(`${CACHE_DIR}minified_${cacheKey}`).catch(() => {}); const modifiedSince = req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']); - if (stats != null && stats.mtime && responseCache[cacheKey]) { + if (stats != null && stats instanceof Object && "mtime" in stats && responseCache[cacheKey]) { req.headers['if-modified-since'] = stats.mtime.toUTCString(); } else { delete req.headers['if-modified-since']; @@ -200,4 +212,4 @@ module.exports = class CachingMiddleware { next(undefined, req, res); } -}; +} diff --git a/src/node/utils/customError.js b/src/node/utils/customError.ts similarity index 87% rename from src/node/utils/customError.js rename to src/node/utils/customError.ts index 24ad181e6..79cbbd47c 100644 --- a/src/node/utils/customError.js +++ b/src/node/utils/customError.ts @@ -7,7 +7,9 @@ * @class CustomError * @extends {Error} */ -class CustomError extends Error { +export class CustomError extends Error { + code: any; + signal: any; /** * Creates an instance of CustomError. * @param {*} message @@ -20,5 +22,3 @@ class CustomError extends Error { Error.captureStackTrace(this, this.constructor); } } - -module.exports = CustomError; diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.ts similarity index 94% rename from src/node/utils/padDiff.js rename to src/node/utils/padDiff.ts index 4ab276b4b..da8ff6877 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.ts @@ -1,11 +1,12 @@ 'use strict'; -const AttributeMap = require('../../static/js/AttributeMap'); -const Changeset = require('../../static/js/Changeset'); -const attributes = require('../../static/js/attributes'); -const exportHtml = require('./ExportHtml'); +import AttributeMap from '../../static/js/AttributeMap'; +import Changeset from '../../static/js/Changeset'; +import attributes from '../../static/js/attributes'; +import {getHTMLFromAtext} from './ExportHtml'; +import {PadDiffModel} from "ep_etherpad-lite/node/models/PadDiffModel"; -function PadDiff(pad, fromRev, toRev) { +export const PadDiff = (pad, fromRev, toRev)=> { // check parameters if (!pad || !pad.id || !pad.atext || !pad.pool) { throw new Error('Invalid pad'); @@ -14,10 +15,16 @@ function PadDiff(pad, fromRev, toRev) { const range = pad.getValidRevisionRange(fromRev, toRev); if (!range) throw new Error(`Invalid revision range. startRev: ${fromRev} endRev: ${toRev}`); + // FIXME How to fix this? + // @ts-ignore this._pad = pad; + // @ts-ignore this._fromRev = range.startRev; + // @ts-ignore this._toRev = range.endRev; + // @ts-ignore this._html = null; + // @ts-ignore this._authors = []; } @@ -108,9 +115,11 @@ PadDiff.prototype._getChangesetsInBulk = async function (startRev, count) { return {changesets, authors}; }; -PadDiff.prototype._addAuthors = function (authors) { - const self = this; - +PadDiff.prototype._addAuthors = (authors)=> { + let self: undefined|PadDiffModel = this; + if(!self){ + self = {_authors: []} + } // add to array if not in the array authors.forEach((author) => { if (self._authors.indexOf(author) === -1) { @@ -187,7 +196,7 @@ PadDiff.prototype.getHtml = async function () { const authorColors = await this._pad.getAllAuthorColors(); // convert the atext to html - this._html = await exportHtml.getHTMLFromAtext(this._pad, atext, authorColors); + this._html = await getHTMLFromAtext(this._pad, atext, authorColors); return this._html; }; @@ -198,6 +207,10 @@ PadDiff.prototype.getAuthors = async function () { if (this._html == null) { await this.getHtml(); } + let self: undefined|PadDiffModel = this; + if(!self){ + self = {_authors: []} + } return self._authors; }; diff --git a/src/node/utils/path_exists.js b/src/node/utils/path_exists.ts similarity index 71% rename from src/node/utils/path_exists.js rename to src/node/utils/path_exists.ts index 0b4c8fe94..91b6dd127 100644 --- a/src/node/utils/path_exists.js +++ b/src/node/utils/path_exists.ts @@ -1,7 +1,7 @@ 'use strict'; -const fs = require('fs'); +import fs from 'fs'; -const check = (path) => { +export const check = (path) => { const existsSync = fs.statSync || fs.existsSync || path.existsSync; let result; @@ -11,6 +11,4 @@ const check = (path) => { result = false; } return result; -}; - -module.exports = check; +} diff --git a/src/node/utils/promises.js b/src/node/utils/promises.ts similarity index 93% rename from src/node/utils/promises.js rename to src/node/utils/promises.ts index bc9f8c2dc..9ebb07da4 100644 --- a/src/node/utils/promises.js +++ b/src/node/utils/promises.ts @@ -7,7 +7,7 @@ // `predicate`. Resolves to `undefined` if none of the Promises satisfy `predicate`, or if // `promises` is empty. If `predicate` is nullish, the truthiness of the resolved value is used as // the predicate. -exports.firstSatisfies = (promises, predicate) => { +export const firstSatisfies = (promises, predicate) => { if (predicate == null) predicate = (x) => x; // Transform each original Promise into a Promise that never resolves if the original resolved @@ -42,7 +42,7 @@ exports.firstSatisfies = (promises, predicate) => { // `total` is greater than `concurrency`, then `concurrency` Promises will be created right away, // and each remaining Promise will be created once one of the earlier Promises resolves.) This async // function resolves once all `total` Promises have resolved. -exports.timesLimit = async (total, concurrency, promiseCreator) => { +export const timesLimit = async (total, concurrency, promiseCreator) => { if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive'); let next = 0; const addAnother = () => promiseCreator(next++).finally(() => { @@ -55,11 +55,14 @@ exports.timesLimit = async (total, concurrency, promiseCreator) => { await Promise.all(promises); }; + /** * An ordinary Promise except the `resolve` and `reject` executor functions are exposed as * properties. */ -class Gate extends Promise { +//FIXME Why is the constructor diviating from the Promise constructor? +// @ts-ignore +export class Gate extends Promise { // Coax `.then()` into returning an ordinary Promise, not a Gate. See // https://stackoverflow.com/a/65669070 for the rationale. static get [Symbol.species]() { return Promise; } @@ -73,4 +76,3 @@ class Gate extends Promise { Object.assign(this, props); } } -exports.Gate = Gate; diff --git a/src/node/utils/randomstring.js b/src/node/utils/randomstring.ts similarity index 100% rename from src/node/utils/randomstring.js rename to src/node/utils/randomstring.ts diff --git a/src/node/utils/run_cmd.js b/src/node/utils/run_cmd.ts similarity index 81% rename from src/node/utils/run_cmd.js rename to src/node/utils/run_cmd.ts index bf5515c84..716e528b4 100644 --- a/src/node/utils/run_cmd.js +++ b/src/node/utils/run_cmd.ts @@ -1,12 +1,12 @@ 'use strict'; -const spawn = require('cross-spawn'); -const log4js = require('log4js'); -const path = require('path'); -const settings = require('./Settings'); - +import spawn from 'cross-spawn'; +import log4js from 'log4js'; +import path from 'path'; +import {root} from "./Settings"; +import {CMDOptions, CMDPromise} from '../models/CMDOptions' const logger = log4js.getLogger('runCmd'); - +import {CustomError} from './customError' const logLines = (readable, logLineFn) => { readable.setEncoding('utf8'); // The process won't necessarily write full lines every time -- it might write a part of a line @@ -69,33 +69,40 @@ const logLines = (readable, logLineFn) => { * - `stderr`: Similar to `stdout` but for stderr. * - `child`: The ChildProcess object. */ -module.exports = exports = (args, opts = {}) => { +export const exportCMD: (args: string[], opts:CMDOptions)=>void = async (args, opts = { + cwd: undefined, + stdio: undefined, + env: undefined +}) => { logger.debug(`Executing command: ${args.join(' ')}`); - - opts = {cwd: settings.root, ...opts}; + opts = {cwd: root, ...opts}; logger.debug(`cwd: ${opts.cwd}`); // Log stdout and stderr by default. const stdio = Array.isArray(opts.stdio) ? opts.stdio.slice() // Copy to avoid mutating the caller's array. - : typeof opts.stdio === 'function' ? [null, opts.stdio, opts.stdio] - : opts.stdio === 'string' ? [null, 'string', 'string'] - : Array(3).fill(opts.stdio); + : typeof opts.stdio === 'function' ? [null, opts.stdio, opts.stdio] + : opts.stdio === 'string' ? [null, 'string', 'string'] + : Array(3).fill(opts.stdio); const cmdLogger = log4js.getLogger(`runCmd|${args[0]}`); - if (stdio[1] == null) stdio[1] = (line) => cmdLogger.info(line); - if (stdio[2] == null) stdio[2] = (line) => cmdLogger.error(line); + if (stdio[1] == null && stdio instanceof Array) stdio[1] = (line) => cmdLogger.info(line); + if (stdio[2] == null && stdio instanceof Array) stdio[2] = (line) => cmdLogger.error(line); const stdioLoggers = []; const stdioSaveString = []; for (const fd of [1, 2]) { if (typeof stdio[fd] === 'function') { stdioLoggers[fd] = stdio[fd]; - stdio[fd] = 'pipe'; + if (stdio instanceof Array) + stdio[fd] = 'pipe'; } else if (stdio[fd] === 'string') { stdioSaveString[fd] = true; - stdio[fd] = 'pipe'; + if (stdio instanceof Array) + stdio[fd] = 'pipe'; } } - opts.stdio = stdio; + if (opts.stdio instanceof Array) { + opts.stdio = stdio; + } // On Windows the PATH environment var might be spelled "Path". const pathVarName = @@ -107,8 +114,8 @@ module.exports = exports = (args, opts = {}) => { opts.env = { ...env, // Copy env to avoid modifying process.env or the caller's supplied env. [pathVarName]: [ - path.join(settings.root, 'src', 'node_modules', '.bin'), - path.join(settings.root, 'node_modules', '.bin'), + path.join(root, 'src', 'node_modules', '.bin'), + path.join(root, 'node_modules', '.bin'), ...(PATH ? PATH.split(path.delimiter) : []), ].join(path.delimiter), }; @@ -116,13 +123,15 @@ module.exports = exports = (args, opts = {}) => { // Create an error object to use in case the process fails. This is done here rather than in the // process's `exit` handler so that we get a useful stack trace. - const procFailedErr = new Error(); + const procFailedErr:CustomError = new CustomError({}); const proc = spawn(args[0], args.slice(1), opts); const streams = [undefined, proc.stdout, proc.stderr]; let px; - const p = new Promise((resolve, reject) => { px = {resolve, reject}; }); + const p = await new Promise((resolve, reject) => { + px = {resolve, reject}; + }); [, p.stdout, p.stderr] = streams; p.child = proc; @@ -132,6 +141,8 @@ module.exports = exports = (args, opts = {}) => { if (stdioLoggers[fd] != null) { logLines(streams[fd], stdioLoggers[fd]); } else if (stdioSaveString[fd]) { + //FIXME How to solve this? + // @ts-ignore p[[null, 'stdout', 'stderr'][fd]] = stdioStringPromises[fd] = (async () => { const chunks = []; for await (const chunk of streams[fd]) chunks.push(chunk); diff --git a/src/node/utils/sanitizePathname.js b/src/node/utils/sanitizePathname.ts similarity index 94% rename from src/node/utils/sanitizePathname.js rename to src/node/utils/sanitizePathname.ts index 61b611166..b9e492661 100644 --- a/src/node/utils/sanitizePathname.js +++ b/src/node/utils/sanitizePathname.ts @@ -1,10 +1,10 @@ 'use strict'; -const path = require('path'); +import path from 'path'; // Normalizes p and ensures that it is a relative path that does not reach outside. See // https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context. -module.exports = (p, pathApi = path) => { +export default (p, pathApi = path) => { // The documentation for path.normalize() says that it resolves '..' and '.' segments. The word // "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might // not be the same thing as 'b'. Most path normalization functions from other libraries (e.g., @@ -20,4 +20,4 @@ module.exports = (p, pathApi = path) => { // pathname would not be normalized away before being converted to '../'. if (pathApi.sep === '\\') p = p.replace(/\\/g, '/'); return p; -}; +} diff --git a/src/node/utils/toolbar.js b/src/node/utils/toolbar.ts similarity index 98% rename from src/node/utils/toolbar.js rename to src/node/utils/toolbar.ts index 40a476878..57aed942f 100644 --- a/src/node/utils/toolbar.js +++ b/src/node/utils/toolbar.ts @@ -2,7 +2,7 @@ /** * The Toolbar Module creates and renders the toolbars and buttons */ -const _ = require('underscore'); +import _ from 'underscore'; const removeItem = (array, what) => { let ax; @@ -12,13 +12,13 @@ const removeItem = (array, what) => { return array; }; -const defaultButtonAttributes = (name, overrides) => ({ +const defaultButtonAttributes = (name, overrides?) => ({ command: name, localizationId: `pad.toolbar.${name}.title`, class: `buttonicon buttonicon-${name}`, }); -const tag = (name, attributes, contents) => { +const tag = (name, attributes, contents?) => { const aStr = tagAttributes(attributes); if (_.isString(contents) && contents.length > 0) { diff --git a/src/package-lock.json b/src/package-lock.json index 34b695265..dd27c26e2 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -58,6 +58,8 @@ }, "devDependencies": { "@types/express": "4.17.17", + "@types/jquery": "^3.5.16", + "@types/js-cookie": "^3.0.3", "@types/node": "^20.3.1", "concurrently": "^8.2.0", "eslint": "^8.14.0", @@ -1081,6 +1083,21 @@ "@types/unist": "*" } }, + "node_modules/@types/jquery": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz", + "integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==", + "dev": true, + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/js-cookie": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.3.tgz", + "integrity": "sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -1181,6 +1198,12 @@ "@types/node": "*" } }, + "node_modules/@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, "node_modules/@types/stoppable": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.1.tgz", diff --git a/src/package.json b/src/package.json index b1062eb7b..6ade4d3d1 100644 --- a/src/package.json +++ b/src/package.json @@ -78,23 +78,25 @@ "etherpad-lite": "node/server.js" }, "devDependencies": { + "@types/express": "4.17.17", + "@types/jquery": "^3.5.16", + "@types/js-cookie": "^3.0.3", + "@types/node": "^20.3.1", + "concurrently": "^8.2.0", "eslint": "^8.14.0", "eslint-config-etherpad": "^3.0.13", "etherpad-cli-client": "^2.0.1", "mocha": "^9.2.2", "mocha-froth": "^0.2.10", "nodeify": "^1.0.1", + "nodemon": "^2.0.22", "openapi-schema-validation": "^0.4.2", "selenium-webdriver": "^4.10.0", "set-cookie-parser": "^2.4.8", "sinon": "^13.0.2", "split-grid": "^1.0.11", "supertest": "^6.3.3", - "typescript": "^4.9.5", - "@types/node": "^20.3.1", - "@types/express": "4.17.17", - "concurrently": "^8.2.0", - "nodemon": "^2.0.22" + "typescript": "^4.9.5" }, "engines": { "node": ">=14.15.0", diff --git a/src/static/js/ace2_common.js b/src/static/js/ace2_common.ts similarity index 78% rename from src/static/js/ace2_common.js rename to src/static/js/ace2_common.ts index c1dab5cfd..683dace3a 100644 --- a/src/static/js/ace2_common.js +++ b/src/static/js/ace2_common.ts @@ -22,11 +22,11 @@ * limitations under the License. */ -const isNodeText = (node) => (node.nodeType === 3); +export const isNodeText = (node) => (node.nodeType === 3); -const getAssoc = (obj, name) => obj[`_magicdom_${name}`]; +export const getAssoc = (obj, name) => obj[`_magicdom_${name}`]; -const setAssoc = (obj, name, value) => { +export const setAssoc = (obj, name, value) => { // note that in IE designMode, properties of a node can get // copied to new nodes that are spawned during editing; also, // properties representable in HTML text can survive copy-and-paste @@ -38,7 +38,7 @@ const setAssoc = (obj, name, value) => { // between false and true, a number between 0 and numItems inclusive. -const binarySearch = (numItems, func) => { +export const binarySearch = (numItems, func) => { if (numItems < 1) return 0; if (func(0)) return 0; if (!func(numItems - 1)) return numItems; @@ -52,17 +52,10 @@ const binarySearch = (numItems, func) => { return high; }; -const binarySearchInfinite = (expectedLength, func) => { +export const binarySearchInfinite = (expectedLength, func) => { let i = 0; while (!func(i)) i += expectedLength; return binarySearch(i, func); }; -const noop = () => {}; - -exports.isNodeText = isNodeText; -exports.getAssoc = getAssoc; -exports.setAssoc = setAssoc; -exports.binarySearch = binarySearch; -exports.binarySearchInfinite = binarySearchInfinite; -exports.noop = noop; +export const noop = () => {}; diff --git a/src/static/js/pad_utils.js b/src/static/js/pad_utils.ts similarity index 92% rename from src/static/js/pad_utils.js rename to src/static/js/pad_utils.ts index e10841f50..537f99169 100644 --- a/src/static/js/pad_utils.js +++ b/src/static/js/pad_utils.ts @@ -22,13 +22,13 @@ * limitations under the License. */ -const Security = require('./security'); +import Security from './security'; /** * Generates a random String with the given length. Is needed to generate the Author, Group, * readonly, session Ids */ -const randomString = (len) => { +const randomString = (len?) => { const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; let randomstring = ''; len = len || 20; @@ -91,7 +91,9 @@ const urlRegex = (() => { // https://stackoverflow.com/a/68957976 const base64url = /^(?=(?:.{4})*$)[A-Za-z0-9_-]*(?:[AQgw]==|[AEIMQUYcgkosw048]=)?$/; -const padutils = { +export const padutils = { + setupGlobalExceptionHandler: undefined, + /** * Prints a warning message followed by a stack trace (to make it easier to figure out what code * is using the deprecated function). @@ -107,25 +109,32 @@ const padutils = { * @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no * logger is set), with a stack trace appended if available. */ - warnDeprecated: (...args) => { - if (padutils.warnDeprecated.disabledForTestingOnly) return; + warnDeprecated: (...args) => { + if ("disabledForTestingOnly" in padutils.warnDeprecated) return; const err = new Error(); if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnDeprecated); err.name = ''; // Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam. - if (typeof err.stack === 'string') { - if (padutils.warnDeprecated._rl == null) { + if (typeof err.stack === 'string' && "_rl" in padutils.warnDeprecated) { padutils.warnDeprecated._rl = {prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000}; - } const rl = padutils.warnDeprecated._rl; - const now = rl.now(); - const prev = rl.prevs.get(err.stack); - if (prev != null && now - prev < rl.period) return; - rl.prevs.set(err.stack, now); + if (rl instanceof Object && "now" in rl && "prevs" in rl && "period" in rl + && rl.now instanceof Function && rl.prevs instanceof Map && rl.period instanceof Number){ + const now = rl.now(); + const prev = rl.prevs.get(err.stack); + if (prev != null && now - prev < rl.period) return; + rl.prevs.set(err.stack, now); + } } if (err.stack) args.push(err.stack); - (padutils.warnDeprecated.logger || console).warn(...args); + if ("logger" in padutils.warnDeprecated && padutils.warnDeprecated.logger instanceof Object + && "warn" in padutils.warnDeprecated.logger && padutils.warnDeprecated.logger.warn instanceof Function){ + padutils.warnDeprecated.logger.warn(...args) + } + else{ + console.warn(...args) + } }, escapeHtml: (x) => Security.escapeHTML(String(x)), @@ -350,7 +359,11 @@ const padutils = { * Returns a string that can be used in the `token` cookie as a secret that authenticates a * particular author. */ - generateAuthorToken: () => `t.${randomString()}`, + generateAuthorToken: () => { + const randomAuthToken = randomString() + + return `t.+${randomAuthToken}` + } }; let globalExceptionHandler = null; @@ -400,6 +413,8 @@ padutils.setupGlobalExceptionHandler = () => { .append(txt(`UserAgent: ${navigator.userAgent}`)).append($('
')), ]; + //FIXME gritter not defined + // @ts-ignore $.gritter.add({ title: 'An error occurred', text: errorMsg, @@ -429,7 +444,7 @@ padutils.setupGlobalExceptionHandler = () => { } }; -padutils.binarySearch = require('./ace2_common').binarySearch; +import {binarySearch} from '../../static/js/ace2_common'; // https://stackoverflow.com/a/42660748 const inThirdPartyIframe = () => { @@ -439,11 +454,12 @@ const inThirdPartyIframe = () => { return true; } }; +import cookies from 'js-cookie/dist/js.cookie'; // This file is included from Node so that it can reuse randomString, but Node doesn't have a global // window object. if (typeof window !== 'undefined') { - exports.Cookies = require('js-cookie/dist/js.cookie').withAttributes({ + cookies.withAttributes({ // Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case // use `SameSite=None`. For iframes from another site, only `None` has a chance of working // because the cookies are third-party (not same-site). Many browsers/users block third-party @@ -456,5 +472,3 @@ if (typeof window !== 'undefined') { secure: window.location.protocol === 'https:', }); } -exports.randomString = randomString; -exports.padutils = padutils; diff --git a/src/tsconfig.json b/src/tsconfig.json index d58a717dc..b99c6c15a 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,5 +1,7 @@ { "compilerOptions": { + "typeRoots": ["node_modules/@types"], + "types": ["node", "jquery"], "module": "commonjs", "esModuleInterop": true, "target": "es6",