diff --git a/src/node/db/PadManager.ts b/src/node/db/PadManager.ts index 4a447f850..c0c4b0f6a 100644 --- a/src/node/db/PadManager.ts +++ b/src/node/db/PadManager.ts @@ -105,7 +105,7 @@ const padList = new class { * @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if * applicable). */ -exports.getPad = async (id: string, text: string, authorId:string = '') => { +exports.getPad = async (id: string, text: string|null, authorId:string = '') => { // check if this is a valid padId if (!exports.isValidPadId(id)) { throw new CustomError(`${id} is not a valid padId`, 'apierror'); diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.ts similarity index 91% rename from src/node/handler/APIHandler.js rename to src/node/handler/APIHandler.ts index 3b5a3fb63..616d20675 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.ts @@ -19,19 +19,21 @@ * limitations under the License. */ +import {MapArrayType} from "../types/MapType"; + const absolutePaths = require('../utils/AbsolutePaths'); -const fs = require('fs'); +import fs from 'fs'; const api = require('../db/API'); -const log4js = require('log4js'); +import log4js from 'log4js'; const padManager = require('../db/PadManager'); const randomString = require('../utils/randomstring'); const argv = require('../utils/Cli').argv; -const createHTTPError = require('http-errors'); +import createHTTPError from 'http-errors'; const apiHandlerLogger = log4js.getLogger('APIHandler'); // ensure we have an apikey -let apikey = null; +let apikey:string|null = null; const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt'); try { @@ -41,11 +43,11 @@ try { apiHandlerLogger.info( `Api key file "${apikeyFilename}" not found. Creating with random contents.`); apikey = randomString(32); - fs.writeFileSync(apikeyFilename, apikey, 'utf8'); + fs.writeFileSync(apikeyFilename, apikey!, 'utf8'); } // a list of all functions -const version = {}; +const version:MapArrayType = {}; version['1'] = { createGroup: [], @@ -163,6 +165,14 @@ exports.latestApiVersion = '1.3.0'; // exports the versions so it can be used by the new Swagger endpoint exports.version = version; + +type APIFields = { + apikey: string; + api_key: string; + padID: string; + padName: string; +} + /** * Handles a HTTP API call * @param {String} apiVersion the version of the api @@ -171,7 +181,7 @@ exports.version = version; * @req express request object * @res express response object */ -exports.handle = async function (apiVersion, functionName, fields) { +exports.handle = async function (apiVersion: string, functionName: string, fields: APIFields) { // say goodbye if this is an unknown API version if (!(apiVersion in version)) { throw new createHTTPError.NotFound('no such api version'); @@ -185,7 +195,7 @@ exports.handle = async function (apiVersion, functionName, fields) { // check the api key! fields.apikey = fields.apikey || fields.api_key; - if (fields.apikey !== apikey.trim()) { + if (fields.apikey !== apikey!.trim()) { throw new createHTTPError.Unauthorized('no or wrong API Key'); } @@ -201,6 +211,7 @@ exports.handle = async function (apiVersion, functionName, fields) { } // put the function parameters in an array + // @ts-ignore const functionParams = version[apiVersion][functionName].map((field) => fields[field]); // call the api function diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.ts similarity index 96% rename from src/node/handler/ExportHandler.js rename to src/node/handler/ExportHandler.ts index 250221d18..0bf57e2d1 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.ts @@ -23,11 +23,11 @@ const exporthtml = require('../utils/ExportHtml'); const exporttxt = require('../utils/ExportTxt'); const exportEtherpad = require('../utils/ExportEtherpad'); -const fs = require('fs'); +import fs from 'fs'; const settings = require('../utils/Settings'); -const os = require('os'); +import os from 'os'; const hooks = require('../../static/js/pluginfw/hooks'); -const util = require('util'); +import util from 'util'; const { checkValidRev } = require('../utils/checkValidRev'); const fsp_writeFile = util.promisify(fs.writeFile); @@ -43,7 +43,7 @@ const tempDirectory = os.tmpdir(); * @param {String} readOnlyId the read only id of the pad to export * @param {String} type the type to export */ -exports.doExport = async (req, res, padId, readOnlyId, type) => { +exports.doExport = async (req: any, res: any, padId: string, readOnlyId: string, type:string) => { // avoid naming the read-only file as the original pad's id let fileName = readOnlyId ? readOnlyId : padId; diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.ts similarity index 93% rename from src/node/handler/ImportHandler.js rename to src/node/handler/ImportHandler.ts index 35ceddfdd..330a2d6f9 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.ts @@ -23,21 +23,22 @@ const padManager = require('../db/PadManager'); const padMessageHandler = require('./PadMessageHandler'); -const fs = require('fs').promises; -const path = require('path'); +import {promises as fs} from 'fs'; +import path from 'path'; const settings = require('../utils/Settings'); const {Formidable} = require('formidable'); -const os = require('os'); +import os from 'os'; const importHtml = require('../utils/ImportHtml'); const importEtherpad = require('../utils/ImportEtherpad'); -const log4js = require('log4js'); +import log4js from 'log4js'; const hooks = require('../../static/js/pluginfw/hooks.js'); const logger = log4js.getLogger('ImportHandler'); // `status` must be a string supported by `importErrorMessage()` in `src/static/js/pad_impexp.js`. class ImportError extends Error { - constructor(status, ...args) { + status: string; + constructor(status: string, ...args:any) { super(...args); if (Error.captureStackTrace) Error.captureStackTrace(this, ImportError); this.name = 'ImportError'; @@ -47,15 +48,15 @@ class ImportError extends Error { } } -const rm = async (path) => { +const rm = async (path: string) => { try { await fs.unlink(path); - } catch (err) { + } catch (err:any) { if (err.code !== 'ENOENT') throw err; } }; -let converter = null; +let converter:any = null; let exportExtension = 'htm'; // load abiword only if it is enabled and if soffice is disabled @@ -78,7 +79,7 @@ const tmpDirectory = os.tmpdir(); * @param {String} padId the pad id to export * @param {String} authorId the author id to use for the import */ -const doImport = async (req, res, padId, authorId) => { +const doImport = async (req:any, res:any, padId:string, authorId:string) => { // pipe to a file // convert file to html via abiword or soffice // set html in the pad @@ -98,7 +99,7 @@ const doImport = async (req, res, padId, authorId) => { let fields; try { [fields, files] = await form.parse(req); - } catch (err) { + } catch (err:any) { logger.warn(`Import failed due to form error: ${err.stack || err}`); if (err.code === Formidable.formidableErrors.biggerThanMaxFileSize) { throw new ImportError('maxFileSize'); @@ -136,7 +137,7 @@ const doImport = async (req, res, padId, authorId) => { const destFile = path.join(tmpDirectory, `etherpad_import_${randNum}.${exportExtension}`); const context = {srcFile, destFile, fileEnding, padId, ImportError}; - const importHandledByPlugin = (await hooks.aCallAll('import', context)).some((x) => x); + const importHandledByPlugin = (await hooks.aCallAll('import', context)).some((x:string) => x); const fileIsEtherpad = (fileEnding === '.etherpad'); const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm'); const fileIsTXT = (fileEnding === '.txt'); @@ -169,7 +170,7 @@ const doImport = async (req, res, padId, authorId) => { } else { try { await converter.convertFile(srcFile, destFile, exportExtension); - } catch (err) { + } catch (err:any) { logger.warn(`Converting Error: ${err.stack || err}`); throw new ImportError('convertFailed'); } @@ -210,7 +211,7 @@ const doImport = async (req, res, padId, authorId) => { if (importHandledByPlugin || useConverter || fileIsHTML) { try { await importHtml.setPadHTML(pad, text, authorId); - } catch (err) { + } catch (err:any) { logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`); } } else { @@ -245,14 +246,14 @@ const doImport = async (req, res, padId, authorId) => { * @param {String} authorId the author id to use for the import * @return {Promise} a promise */ -exports.doImport = async (req, res, padId, authorId = '') => { +exports.doImport = async (req:any, res:any, padId:string, authorId:string = '') => { let httpStatus = 200; let code = 0; let message = 'ok'; let directDatabaseAccess; try { directDatabaseAccess = await doImport(req, res, padId, authorId); - } catch (err) { + } catch (err:any) { const known = err instanceof ImportError && err.status; if (!known) logger.error(`Internal error during import: ${err.stack || err}`); httpStatus = known ? 400 : 500; diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.ts similarity index 93% rename from src/node/handler/PadMessageHandler.js rename to src/node/handler/PadMessageHandler.ts index 8b9d6e068..bbeef736a 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.ts @@ -19,6 +19,8 @@ * limitations under the License. */ +import {MapArrayType} from "../types/MapType"; + const AttributeMap = require('../../static/js/AttributeMap'); const padManager = require('../db/PadManager'); const Changeset = require('../../static/js/Changeset'); @@ -37,16 +39,19 @@ const accessLogger = log4js.getLogger('access'); const hooks = require('../../static/js/pluginfw/hooks.js'); const stats = require('../stats') const assert = require('assert').strict; -const {RateLimiterMemory} = require('rate-limiter-flexible'); +import {RateLimiterMemory} from 'rate-limiter-flexible'; +import {ChangesetRequest, PadUserInfo, SocketClientRequest} from "../types/SocketClientRequest"; +import {APool, AText, PadAuthor, PadType} from "../types/PadType"; +import {ChangeSet} from "../types/ChangeSet"; const webaccess = require('../hooks/express/webaccess'); const { checkValidRev } = require('../utils/checkValidRev'); -let rateLimiter; -let socketio = null; +let rateLimiter:any; +let socketio:any = null; hooks.deprecationNotices.clientReady = 'use the userJoin hook instead'; -const addContextToError = (err, pfx) => { +const addContextToError = (err:any, pfx:string) => { const newErr = new Error(`${pfx}${err.message}`, {cause: err}); if (Error.captureStackTrace) Error.captureStackTrace(newErr, addContextToError); // Check for https://github.com/tc39/proposal-error-cause support, available in Node.js >= v16.10. @@ -80,7 +85,7 @@ exports.socketio = () => { * - readonly: Whether the client has read-only access (true) or read/write access (false). * - rev: The last revision that was sent to the client. */ -const sessioninfos = {}; +const sessioninfos:MapArrayType = {}; exports.sessioninfos = sessioninfos; stats.gauge('totalUsers', () => socketio ? socketio.sockets.size : 0); @@ -97,11 +102,13 @@ stats.gauge('activePads', () => { * Processes one task at a time per channel. */ class Channels { + private readonly _exec: (ch:any, task:any) => any; + private _promiseChains: Map>; /** * @param {(ch, task) => any} [exec] - Task executor. If omitted, tasks are assumed to be * functions that will be executed with the channel as the only argument. */ - constructor(exec = (ch, task) => task(ch)) { + constructor(exec = (ch: string, task:any) => task(ch)) { this._exec = exec; this._promiseChains = new Map(); } @@ -114,7 +121,7 @@ class Channels { * @param {any} task - The task to give to the executor. * @returns {Promise} The value returned by the executor. */ - async enqueue(ch, task) { + async enqueue(ch:any, task:any): Promise { const p = (this._promiseChains.get(ch) || Promise.resolve()).then(() => this._exec(ch, task)); const pc = p .catch(() => {}) // Prevent rejections from halting the queue. @@ -136,7 +143,7 @@ const padChannels = new Channels((ch, {socket, message}) => handleUserChanges(so * This Method is called by server.ts to tell the message handler on which socket it should send * @param socket_io The Socket */ -exports.setSocketIO = (socket_io) => { +exports.setSocketIO = (socket_io:any) => { socketio = socket_io; }; @@ -144,7 +151,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) => { +exports.handleConnect = (socket:any) => { stats.meter('connects').mark(); // Initialize sessioninfos for this new session @@ -154,7 +161,7 @@ exports.handleConnect = (socket) => { /** * Kicks all sessions from a pad */ -exports.kickSessionsFromPad = (padID) => { +exports.kickSessionsFromPad = (padID: string) => { if (typeof socketio.sockets.clients !== 'object') return; // skip if there is nobody on this pad @@ -168,13 +175,13 @@ exports.kickSessionsFromPad = (padID) => { * Handles the disconnection of a user * @param socket the socket.io Socket object for the client */ -exports.handleDisconnect = async (socket) => { +exports.handleDisconnect = async (socket:any) => { stats.meter('disconnects').mark(); const session = sessioninfos[socket.id]; delete sessioninfos[socket.id]; // session.padId can be nullish if the user disconnects before sending CLIENT_READY. if (!session || !session.author || !session.padId) return; - const {session: {user} = {}} = socket.client.request; + const {session: {user} = {}} = socket.client.request as SocketClientRequest; /* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */ accessLogger.info('[LEAVE]' + ` pad:${session.padId}` + @@ -206,7 +213,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) => { +exports.handleMessage = async (socket:any, message:typeof ChatMessage) => { const env = process.env.NODE_ENV || 'development'; if (env === 'production') { @@ -263,7 +270,7 @@ exports.handleMessage = async (socket, message) => { throw new Error(`pre-CLIENT_READY message from IP ${ip}: ${msg}`); } - const {session: {user} = {}} = socket.client.request; + const {session: {user} = {}} = socket.client.request as SocketClientRequest; const {accessStatus, authorID} = await securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, user); if (accessStatus !== 'grant') { @@ -319,7 +326,7 @@ exports.handleMessage = async (socket, message) => { } // Call handleMessage hook. If a plugin returns null, the message will be dropped. - if ((await hooks.aCallAll('handleMessage', context)).some((m) => m == null)) { + if ((await hooks.aCallAll('handleMessage', context)).some((m: null|string) => m == null)) { return; } @@ -376,7 +383,7 @@ exports.handleMessage = async (socket, message) => { * @param socket the socket.io Socket object for the client * @param message the message from the client */ -const handleSaveRevisionMessage = async (socket, message) => { +const handleSaveRevisionMessage = async (socket:any, message: string) => { const {padId, author: authorId} = sessioninfos[socket.id]; const pad = await padManager.getPad(padId, null, authorId); await pad.addSavedRevision(pad.head, authorId); @@ -389,7 +396,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) => { +exports.handleCustomObjectMessage = (msg: typeof ChatMessage, sessionID: string) => { if (msg.data.type === 'CUSTOM') { if (sessionID) { // a sessionID is targeted: directly to this sessionID @@ -407,7 +414,7 @@ exports.handleCustomObjectMessage = (msg, sessionID) => { * @param padID {Pad} the pad to which we're sending this message * @param msgString {String} the message we're sending */ -exports.handleCustomMessage = (padID, msgString) => { +exports.handleCustomMessage = (padID: string, msgString:string) => { const time = Date.now(); const msg = { type: 'COLLABROOM', @@ -424,7 +431,7 @@ exports.handleCustomMessage = (padID, msgString) => { * @param socket the socket.io Socket object for the client * @param message the message from the client */ -const handleChatMessage = async (socket, message) => { +const handleChatMessage = async (socket:any, message: typeof ChatMessage) => { const chatMessage = ChatMessage.fromObject(message.data.message); const {padId, author: authorId} = sessioninfos[socket.id]; // Don't trust the user-supplied values. @@ -444,7 +451,7 @@ const handleChatMessage = async (socket, message) => { * @param {string} [padId] - The destination pad ID. Deprecated; pass a chat message * object as the first argument and the destination pad ID as the second argument instead. */ -exports.sendChatMessageToPadClients = async (mt, puId, text = null, padId = null) => { +exports.sendChatMessageToPadClients = async (mt: typeof ChatMessage|number, puId: string, text:string|null = null, padId:string|null = null) => { const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt); padId = mt instanceof ChatMessage ? puId : padId; const pad = await padManager.getPad(padId, null, message.authorId); @@ -465,7 +472,7 @@ exports.sendChatMessageToPadClients = async (mt, puId, text = null, padId = null * @param socket the socket.io Socket object for the client * @param message the message from the client */ -const handleGetChatMessages = async (socket, {data: {start, end}}) => { +const handleGetChatMessages = async (socket:any, {data: {start, end}}:any) => { if (!Number.isInteger(start)) throw new Error(`missing or invalid start: ${start}`); if (!Number.isInteger(end)) throw new Error(`missing or invalid end: ${end}`); const count = end - start; @@ -491,7 +498,7 @@ const handleGetChatMessages = async (socket, {data: {start, end}}) => { * @param socket the socket.io Socket object for the client * @param message the message from the client */ -const handleSuggestUserName = (socket, message) => { +const handleSuggestUserName = (socket:any, message: typeof ChatMessage) => { const {newName, unnamedId} = message.data.payload; if (newName == null) throw new Error('missing newName'); if (unnamedId == null) throw new Error('missing unnamedId'); @@ -511,7 +518,7 @@ const handleSuggestUserName = (socket, message) => { * @param socket the socket.io Socket object for the client * @param message the message from the client */ -const handleUserInfoUpdate = async (socket, {data: {userInfo: {name, colorId}}}) => { +const handleUserInfoUpdate = async (socket:any, {data: {userInfo: {name, colorId}}}: PadUserInfo) => { if (colorId == null) throw new Error('missing colorId'); if (!name) name = null; const session = sessioninfos[socket.id]; @@ -559,7 +566,7 @@ const handleUserInfoUpdate = async (socket, {data: {userInfo: {name, colorId}}}) * @param socket the socket.io Socket object for the client * @param message the message from the client */ -const handleUserChanges = async (socket, message) => { +const handleUserChanges = async (socket:any, message: typeof ChatMessage) => { // This one's no longer pending, as we're gonna process it now stats.counter('pendingEdits').dec(); @@ -658,7 +665,7 @@ const handleUserChanges = async (socket, message) => { thisSession.rev = newRev; if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev); await exports.updatePadClients(pad); - } catch (err) { + } catch (err:any) { socket.emit('message', {disconnect: 'badChangeset'}); stats.meter('failedChangesets').mark(); messageLogger.warn(`Failed to apply USER_CHANGES from author ${thisSession.author} ` + @@ -668,7 +675,7 @@ const handleUserChanges = async (socket, message) => { } }; -exports.updatePadClients = async (pad) => { +exports.updatePadClients = async (pad: PadType) => { // skip this if no-one is on this pad const roomSockets = _getRoomSockets(pad.id); if (roomSockets.length === 0) return; @@ -683,7 +690,7 @@ exports.updatePadClients = async (pad) => { // via async.forEach with sequential for() loop. There is no real // benefits of running this in parallel, // but benefit of reusing cached revision object is HUGE - const revCache = {}; + const revCache:MapArrayType = {}; await Promise.all(roomSockets.map(async (socket) => { const sessioninfo = sessioninfos[socket.id]; @@ -717,7 +724,7 @@ exports.updatePadClients = async (pad) => { }; try { socket.emit('message', msg); - } catch (err) { + } catch (err:any) { messageLogger.error(`Failed to notify user of new revision: ${err.stack || err}`); return; } @@ -730,7 +737,7 @@ exports.updatePadClients = async (pad) => { /** * Copied from the Etherpad Source Code. Don't know what this method does excatly... */ -const _correctMarkersInPad = (atext, apool) => { +const _correctMarkersInPad = (atext: AText, apool: APool) => { const text = atext.text; // collect char positions of line markers (e.g. bullets) in new atext @@ -739,7 +746,7 @@ const _correctMarkersInPad = (atext, apool) => { let offset = 0; for (const op of Changeset.deserializeOps(atext.attribs)) { const attribs = AttributeMap.fromString(op.attribs, apool); - const hasMarker = AttributeManager.lineAttributes.some((a) => attribs.has(a)); + const hasMarker = AttributeManager.lineAttributes.some((a: string) => attribs.has(a)); if (hasMarker) { for (let i = 0; i < op.chars; i++) { if (offset > 0 && text.charAt(offset - 1) !== '\n') { @@ -777,7 +784,7 @@ const _correctMarkersInPad = (atext, apool) => { * @param socket the socket.io Socket object for the client * @param message the message from the client */ -const handleClientReady = async (socket, message) => { +const handleClientReady = async (socket:any, message: typeof ChatMessage) => { const sessionInfo = sessioninfos[socket.id]; if (sessionInfo == null) throw new Error('client disconnected'); assert(sessionInfo.author); @@ -805,8 +812,11 @@ const handleClientReady = async (socket, message) => { const currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber()); // get all author data out of the database (in parallel) - const historicalAuthorData = {}; - await Promise.all(authors.map(async (authorId) => { + const historicalAuthorData:MapArrayType<{ + name: string; + colorId: string; + }> = {}; + await Promise.all(authors.map(async (authorId: string) => { const author = await authorManager.getAuthor(authorId); if (!author) { messageLogger.error(`There is no author for authorId: ${authorId}. ` + @@ -837,7 +847,7 @@ const handleClientReady = async (socket, message) => { } } - const {session: {user} = {}} = socket.client.request; + const {session: {user} = {}} = socket.client.request as SocketClientRequest; /* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */ accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` + ` pad:${sessionInfo.padId}` + @@ -859,7 +869,7 @@ const handleClientReady = async (socket, message) => { // By using client revision, // this below code sends all the revisions missed during the client reconnect const revisionsNeeded = []; - const changesets = {}; + const changesets:MapArrayType = {}; let startNum = message.client_rev + 1; let endNum = pad.getHeadRevisionNumber() + 1; @@ -919,7 +929,7 @@ const handleClientReady = async (socket, message) => { const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); apool = attribsForWire.pool.toJsonable(); atext.attribs = attribsForWire.translated; - } catch (e) { + } catch (e:any) { messageLogger.error(e.stack || e); socket.emit('message', {disconnect: 'corruptPad'}); // pull the brakes throw new Error('corrupt pad'); @@ -927,7 +937,7 @@ const handleClientReady = async (socket, message) => { // Warning: never ever send sessionInfo.padId to the client. If the client is read only you // would open a security hole 1 swedish mile wide... - const clientVars = { + const clientVars:MapArrayType = { skinName: settings.skinName, skinVariants: settings.skinVariants, randomVersionString: settings.randomVersionString, @@ -1078,7 +1088,7 @@ const handleClientReady = async (socket, message) => { /** * Handles a request for a rough changeset, the timeslider client needs it */ -const handleChangesetRequest = async (socket, {data: {granularity, start, requestID}}) => { +const handleChangesetRequest = async (socket:any, {data: {granularity, start, requestID}}: ChangesetRequest) => { if (granularity == null) throw new Error('missing granularity'); if (!Number.isInteger(granularity)) throw new Error('granularity is not an integer'); if (start == null) throw new Error('missing start'); @@ -1090,7 +1100,7 @@ const handleChangesetRequest = async (socket, {data: {granularity, start, reques const headRev = pad.getHeadRevisionNumber(); if (start > headRev) start = headRev; - const data = await getChangesetInfo(pad, start, end, granularity); + const data:MapArrayType = await getChangesetInfo(pad, start, end, granularity); data.requestID = requestID; socket.emit('message', {type: 'CHANGESET_REQ', data}); }; @@ -1099,7 +1109,7 @@ const handleChangesetRequest = async (socket, {data: {granularity, start, reques * Tries to rebuild the getChangestInfo function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 */ -const getChangesetInfo = async (pad, startNum, endNum, granularity) => { +const getChangesetInfo = async (pad: PadType, startNum: number, endNum:number, granularity: number) => { const headRevision = pad.getHeadRevisionNumber(); // calculate the last full endnum @@ -1124,8 +1134,8 @@ const getChangesetInfo = async (pad, startNum, endNum, granularity) => { } // Get all needed db values in parallel. - const composedChangesets = {}; - const revisionDate = []; + const composedChangesets:MapArrayType = {}; + const revisionDate:number[] = []; const [lines] = await Promise.all([ getPadLines(pad, startNum - 1), // Get all needed composite Changesets. @@ -1176,7 +1186,7 @@ const getChangesetInfo = async (pad, startNum, endNum, granularity) => { * Tries to rebuild the getPadLines function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263 */ -const getPadLines = async (pad, revNum) => { +const getPadLines = async (pad: PadType, revNum: number) => { // get the atext let atext; @@ -1196,7 +1206,7 @@ const getPadLines = async (pad, revNum) => { * Tries to rebuild the composePadChangeset function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241 */ -const composePadChangesets = async (pad, startNum, endNum) => { +const composePadChangesets = async (pad: PadType, startNum: number, endNum: number) => { // fetch all changesets we need const headNum = pad.getHeadRevisionNumber(); endNum = Math.min(endNum, headNum + 1); @@ -1210,7 +1220,7 @@ const composePadChangesets = async (pad, startNum, endNum) => { } // get all changesets - const changesets = {}; + const changesets:MapArrayType = {}; await Promise.all(changesetsNeeded.map( (revNum) => pad.getRevisionChangeset(revNum) .then((changeset) => changesets[revNum] = changeset))); @@ -1234,13 +1244,13 @@ const composePadChangesets = async (pad, startNum, endNum) => { } }; -const _getRoomSockets = (padID) => { +const _getRoomSockets = (padID: string) => { const ns = socketio.sockets; // Default namespace. // We could call adapter.clients(), but that method is unnecessarily asynchronous. Replicate what // it does here, but synchronously to avoid a race condition. This code will have to change when // we update to socket.io v3. const room = ns.adapter.rooms?.get(padID); - + if (!room) return []; return Array.from(room) @@ -1251,15 +1261,15 @@ const _getRoomSockets = (padID) => { /** * Get the number of users in a pad */ -exports.padUsersCount = (padID) => ({ +exports.padUsersCount = (padID:string) => ({ padUsersCount: _getRoomSockets(padID).length, }); /** * Get the list of users in a pad */ -exports.padUsers = async (padID) => { - const padUsers = []; +exports.padUsers = async (padID: string) => { + const padUsers:PadAuthor[] = []; // iterate over all clients (in parallel) await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => { diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.ts similarity index 84% rename from src/node/handler/SocketIORouter.js rename to src/node/handler/SocketIORouter.ts index e34a16603..482276834 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.ts @@ -20,6 +20,8 @@ * limitations under the License. */ +import {MapArrayType} from "../types/MapType"; +import {SocketModule} from "../types/SocketModule"; const log4js = require('log4js'); const settings = require('../utils/Settings'); const stats = require('../../node/stats') @@ -31,15 +33,15 @@ const logger = log4js.getLogger('socket.io'); * key is the component name * value is the component module */ -const components = {}; +const components:MapArrayType = {}; -let io; +let io:any; /** adds a component * @param {string} moduleName * @param {Module} module */ -exports.addComponent = (moduleName, module) => { +exports.addComponent = (moduleName: string, module: SocketModule) => { if (module == null) return exports.deleteComponent(moduleName); components[moduleName] = module; module.setSocketIO(io); @@ -49,22 +51,22 @@ exports.addComponent = (moduleName, module) => { * removes a component * @param {Module} moduleName */ -exports.deleteComponent = (moduleName) => { delete components[moduleName]; }; +exports.deleteComponent = (moduleName: string) => { delete components[moduleName]; }; /** * sets the socket.io and adds event functions for routing * @param {Object} _io the socket.io instance */ -exports.setSocketIO = (_io) => { +exports.setSocketIO = (_io:any) => { io = _io; - io.sockets.on('connection', (socket) => { + io.sockets.on('connection', (socket:any) => { const ip = settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip; logger.debug(`${socket.id} connected from IP ${ip}`); // wrap the original send function to log the messages socket._send = socket.send; - socket.send = (message) => { + socket.send = (message: string) => { logger.debug(`to ${socket.id}: ${JSON.stringify(message)}`); socket._send(message); }; @@ -74,7 +76,7 @@ exports.setSocketIO = (_io) => { components[i].handleConnect(socket); } - socket.on('message', (message, ack = () => {}) => (async () => { + socket.on('message', (message: any, ack: any = () => {}) => (async () => { if (!message.component || !components[message.component]) { throw new Error(`unknown message component: ${message.component}`); } @@ -88,7 +90,7 @@ exports.setSocketIO = (_io) => { ack({name: err.name, message: err.message}); // socket.io can't handle Error objects. })); - socket.on('disconnect', (reason) => { + socket.on('disconnect', (reason: string) => { logger.debug(`${socket.id} disconnected: ${reason}`); // store the lastDisconnect as a timestamp, this is useful if you want to know // when the last user disconnected. If your activePads is 0 and totalUsers is 0 diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.ts similarity index 87% rename from src/node/hooks/express/webaccess.js rename to src/node/hooks/express/webaccess.ts index e0a5bd084..43bdea5fc 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.ts @@ -1,7 +1,10 @@ 'use strict'; -const assert = require('assert').strict; -const log4js = require('log4js'); +import {strict as assert} from "assert"; +import log4js from 'log4js'; +import {SocketClientRequest} from "../../types/SocketClientRequest"; +import {WebAccessTypes} from "../../types/WebAccessTypes"; +import {SettingsUser} from "../../types/SettingsUser"; const httpLogger = log4js.getLogger('http'); const settings = require('../../utils/Settings'); const hooks = require('../../../static/js/pluginfw/hooks'); @@ -10,14 +13,15 @@ const readOnlyManager = require('../../db/ReadOnlyManager'); hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead'; // Promisified wrapper around hooks.aCallFirst. -const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, reject) => { - hooks.aCallFirst(hookName, context, (err, r) => err != null ? reject(err) : resolve(r), pred); +const aCallFirst = (hookName: string, context:any, pred = null) => new Promise((resolve, reject) => { + hooks.aCallFirst(hookName, context, (err:any, r: unknown) => err != null ? reject(err) : resolve(r), pred); }); const aCallFirst0 = - async (hookName, context, pred = null) => (await aCallFirst(hookName, context, pred))[0]; + // @ts-ignore + async (hookName: string, context:any, pred = null) => (await aCallFirst(hookName, context, pred))[0]; -exports.normalizeAuthzLevel = (level) => { +exports.normalizeAuthzLevel = (level: string|boolean) => { if (!level) return false; switch (level) { case true: @@ -32,7 +36,7 @@ exports.normalizeAuthzLevel = (level) => { return false; }; -exports.userCanModify = (padId, req) => { +exports.userCanModify = (padId: string, req: SocketClientRequest) => { if (readOnlyManager.isReadOnlyId(padId)) return false; if (!settings.requireAuthentication) return true; const {session: {user} = {}} = req; @@ -45,7 +49,7 @@ exports.userCanModify = (padId, req) => { // Exported so that tests can set this to 0 to avoid unnecessary test slowness. exports.authnFailureDelayMs = 1000; -const checkAccess = async (req, res, next) => { +const checkAccess = async (req:any, res:any, next: Function) => { const requireAdmin = req.path.toLowerCase().startsWith('/admin'); // /////////////////////////////////////////////////////////////////////////////////////////////// @@ -54,9 +58,9 @@ const checkAccess = async (req, res, next) => { // use the preAuthzFailure hook to override the default 403 error. // /////////////////////////////////////////////////////////////////////////////////////////////// - let results; + let results: null|boolean[]; let skip = false; - const preAuthorizeNext = (...args) => { skip = true; next(...args); }; + const preAuthorizeNext = (...args:any) => { skip = true; next(...args); }; try { results = await aCallFirst('preAuthorize', {req, res, next: preAuthorizeNext}, // This predicate will cause aCallFirst to call the hook functions one at a time until one @@ -64,8 +68,9 @@ const checkAccess = async (req, res, next) => { // page, truthy entries are filtered out before checking to see whether the list is empty. // This prevents plugin authors from accidentally granting admin privileges to the general // public. - (r) => (skip || (r != null && r.filter((x) => (!requireAdmin || !x)).length > 0))); - } catch (err) { + // @ts-ignore + (r) => (skip || (r != null && r.filter((x) => (!requireAdmin || !x)).length > 0))) as boolean[]; + } catch (err:any) { httpLogger.error(`Error in preAuthorize hook: ${err.stack || err.toString()}`); if (!skip) res.status(500).send('Internal Server Error'); return; @@ -87,7 +92,7 @@ const checkAccess = async (req, res, next) => { // This helper is used in steps 2 and 4 below, so it may be called twice per access: once before // authentication is checked and once after (if settings.requireAuthorization is true). const authorize = async () => { - const grant = async (level) => { + const grant = async (level: string|false) => { level = exports.normalizeAuthzLevel(level); if (!level) return false; const user = req.session.user; @@ -132,7 +137,7 @@ const checkAccess = async (req, res, next) => { // /////////////////////////////////////////////////////////////////////////////////////////////// if (settings.users == null) settings.users = {}; - const ctx = {req, res, users: settings.users, next}; + const ctx:WebAccessTypes = {req, res, users: settings.users, next}; // If the HTTP basic auth header is present, extract the username and password so it can be given // to authn plugins. const httpBasicAuth = req.headers.authorization && req.headers.authorization.startsWith('Basic '); @@ -148,7 +153,8 @@ const checkAccess = async (req, res, next) => { } if (!(await aCallFirst0('authenticate', ctx))) { // Fall back to HTTP basic auth. - const {[ctx.username]: {password} = {}} = settings.users; + // @ts-ignore + const {[ctx.username]: {password} = {}} = settings.users as SettingsUser; if (!httpBasicAuth || !ctx.username || @@ -193,6 +199,6 @@ const checkAccess = async (req, res, next) => { * Express middleware to authenticate the user and check authorization. Must be installed after the * express-session middleware. */ -exports.checkAccess = (req, res, next) => { +exports.checkAccess = (req:any, res:any, next:Function) => { checkAccess(req, res, next).catch((err) => next(err || new Error(err))); }; diff --git a/src/node/types/APIHandlerType.ts b/src/node/types/APIHandlerType.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/node/types/ChangeSet.ts b/src/node/types/ChangeSet.ts new file mode 100644 index 000000000..17f38f101 --- /dev/null +++ b/src/node/types/ChangeSet.ts @@ -0,0 +1,3 @@ +export type ChangeSet = { + +} diff --git a/src/node/types/PadType.ts b/src/node/types/PadType.ts index 5f222c969..b344ed8c5 100644 --- a/src/node/types/PadType.ts +++ b/src/node/types/PadType.ts @@ -13,8 +13,12 @@ export type PadType = { getAllAuthorColors: ()=>Promise>, remove: ()=>Promise, text: ()=>string, - setText: (text: string)=>Promise, + setText: (text: string, authorId?: string)=>Promise, appendText: (text: string)=>Promise, + getHeadRevisionNumber: ()=>number, + getRevisionDate: (rev: number)=>Promise, + getRevisionChangeset: (rev: number)=>Promise, + appendRevision: (changeset: AChangeSet, author: string)=>Promise, } diff --git a/src/node/types/SocketAcknowledge.ts b/src/node/types/SocketAcknowledge.ts new file mode 100644 index 000000000..a55f77219 --- /dev/null +++ b/src/node/types/SocketAcknowledge.ts @@ -0,0 +1,3 @@ +export type SocketAcknowledge = { + +} diff --git a/src/node/types/SocketClientRequest.ts b/src/node/types/SocketClientRequest.ts new file mode 100644 index 000000000..07c015fc5 --- /dev/null +++ b/src/node/types/SocketClientRequest.ts @@ -0,0 +1,30 @@ +export type SocketClientRequest = { + session: { + user: { + username: string; + readOnly: boolean; + padAuthorizations: { + [key: string]: string; + } + } + } +} + + +export type PadUserInfo = { + data: { + userInfo: { + name: string|null; + colorId: string; + } + } +} + + +export type ChangesetRequest = { + data: { + granularity: number; + start: number; + requestID: string; + } +} diff --git a/src/node/types/SocketModule.ts b/src/node/types/SocketModule.ts new file mode 100644 index 000000000..fab6b572e --- /dev/null +++ b/src/node/types/SocketModule.ts @@ -0,0 +1,3 @@ +export type SocketModule = { + setSocketIO: (io: any) => void; +} diff --git a/src/node/types/WebAccessTypes.ts b/src/node/types/WebAccessTypes.ts new file mode 100644 index 000000000..b351f059f --- /dev/null +++ b/src/node/types/WebAccessTypes.ts @@ -0,0 +1,10 @@ +import {SettingsUser} from "./SettingsUser"; + +export type WebAccessTypes = { + username?: string|null; + password?: string; + req:any; + res:any; + next:any; + users: SettingsUser; +} diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.ts similarity index 88% rename from src/node/utils/ImportEtherpad.js rename to src/node/utils/ImportEtherpad.ts index da7e750ff..50b9a43d5 100644 --- a/src/node/utils/ImportEtherpad.js +++ b/src/node/utils/ImportEtherpad.ts @@ -1,5 +1,7 @@ 'use strict'; +import {APool} from "../types/PadType"; + /** * 2014 John McLear (Etherpad Foundation / McLear Ltd) * @@ -22,17 +24,17 @@ 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'); +import log4js from 'log4js'; const supportedElems = require('../../static/js/contentcollector').supportedElems; -const ueberdb = require('ueberdb2'); +import ueberdb from 'ueberdb2'; const logger = log4js.getLogger('ImportEtherpad'); -exports.setPadRaw = async (padId, r, authorId = '') => { +exports.setPadRaw = async (padId: string, r: string, authorId = '') => { const records = JSON.parse(r); // get supported block Elements from plugins, we will use this later. - hooks.callAll('ccRegisterBlockElements').forEach((element) => { + hooks.callAll('ccRegisterBlockElements').forEach((element:any) => { supportedElems.add(element); }); @@ -43,8 +45,8 @@ exports.setPadRaw = async (padId, r, authorId = '') => { 'pad', ]; - let originalPadId = null; - const checkOriginalPadId = (padId) => { + let originalPadId:string|null = null; + const checkOriginalPadId = (padId: string) => { if (originalPadId == null) originalPadId = padId; if (originalPadId !== padId) throw new Error('unexpected pad ID in record'); }; @@ -57,7 +59,10 @@ exports.setPadRaw = async (padId, r, authorId = '') => { const padDb = new ueberdb.Database('memory', {data}); await padDb.init(); try { - const processRecord = async (key, value) => { + const processRecord = async (key:string, value: null|{ + padIDs: string|Record, + pool: APool + }) => { if (!value) return; const keyParts = key.split(':'); const [prefix, id] = keyParts; @@ -79,7 +84,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => { if (prefix === 'pad' && keyParts.length === 2) { const pool = new AttributePool().fromJsonable(value.pool); const unsupportedElements = new Set(); - pool.eachAttrib((k, v) => { + pool.eachAttrib((k: string, v:any) => { if (!supportedElems.has(k)) unsupportedElements.add(k); }); if (unsupportedElements.size) { @@ -94,8 +99,10 @@ exports.setPadRaw = async (padId, r, authorId = '') => { `importEtherpad hook function processes it: ${key}`); return; } + // @ts-ignore await padDb.set(key, value); }; + // @ts-ignore const readOps = new Stream(Object.entries(records)).map(([k, v]) => processRecord(k, v)); for (const op of readOps.batch(100).buffer(99)) await op; diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.ts similarity index 93% rename from src/node/utils/ImportHtml.js rename to src/node/utils/ImportHtml.ts index d7b2172b0..57d9cbe4f 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.ts @@ -15,15 +15,16 @@ * limitations under the License. */ -const log4js = require('log4js'); +import log4js from 'log4js'; const Changeset = require('../../static/js/Changeset'); const contentcollector = require('../../static/js/contentcollector'); -const jsdom = require('jsdom'); +import jsdom from 'jsdom'; +import {PadType} from "../types/PadType"; const apiLogger = log4js.getLogger('ImportHtml'); -let processor; +let processor:any; -exports.setPadHTML = async (pad, html, authorId = '') => { +exports.setPadHTML = async (pad: PadType, html:string, authorId = '') => { if (processor == null) { const [{rehype}, {default: minifyWhitespace}] = await Promise.all([import('rehype'), import('rehype-minify-whitespace')]); @@ -46,7 +47,7 @@ exports.setPadHTML = async (pad, html, authorId = '') => { try { // we use a try here because if the HTML is bad it will blow up cc.collectContent(document.body); - } catch (err) { + } catch (err: any) { apiLogger.warn(`Error processing HTML: ${err.stack || err}`); throw err; } diff --git a/src/package.json b/src/package.json index 31b2906b6..f23f716e0 100644 --- a/src/package.json +++ b/src/package.json @@ -81,6 +81,8 @@ "devDependencies": { "@types/async": "^3.2.24", "@types/express": "^4.17.21", + "@types/http-errors": "^2.0.4", + "@types/jsdom": "^21.1.6", "@types/mocha": "^10.0.6", "@types/node": "^20.11.19", "@types/sinon": "^17.0.3", diff --git a/src/tsconfig.json b/src/tsconfig.json index eea5f16fa..179aae429 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ "moduleDetection": "force", - "lib": ["es6"], + "lib": ["ES2023"], /* Language and Environment */ "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ /* Modules */