Added backend in typescript. (#6185)

This commit is contained in:
SamTV12345 2024-02-23 19:48:55 +01:00 committed by GitHub
parent c2b9df3b24
commit 295a2a758b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 211 additions and 118 deletions

View file

@ -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');

View file

@ -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<any> = {};
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

View file

@ -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;

View file

@ -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<void>} 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;

View file

@ -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<any> = {};
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<any, Promise<any>>;
/**
* @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<any>} The value returned by the executor.
*/
async enqueue(ch, task) {
async enqueue(ch:any, task:any): Promise<any> {
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<any> = {};
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<any> = {};
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<any> = {
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<any> = 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<any> = {};
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<ChangeSet> = {};
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) => {

View file

@ -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<any> = {};
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

View file

@ -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)));
};

View file

View file

@ -0,0 +1,3 @@
export type ChangeSet = {
}

View file

@ -13,8 +13,12 @@ export type PadType = {
getAllAuthorColors: ()=>Promise<MapArrayType<string>>,
remove: ()=>Promise<void>,
text: ()=>string,
setText: (text: string)=>Promise<void>,
setText: (text: string, authorId?: string)=>Promise<void>,
appendText: (text: string)=>Promise<void>,
getHeadRevisionNumber: ()=>number,
getRevisionDate: (rev: number)=>Promise<number>,
getRevisionChangeset: (rev: number)=>Promise<AChangeSet>,
appendRevision: (changeset: AChangeSet, author: string)=>Promise<void>,
}

View file

@ -0,0 +1,3 @@
export type SocketAcknowledge = {
}

View file

@ -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;
}
}

View file

@ -0,0 +1,3 @@
export type SocketModule = {
setSocketIO: (io: any) => void;
}

View file

@ -0,0 +1,10 @@
import {SettingsUser} from "./SettingsUser";
export type WebAccessTypes = {
username?: string|null;
password?: string;
req:any;
res:any;
next:any;
users: SettingsUser;
}

View file

@ -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<string, unknown>,
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;

View file

@ -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;
}