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 * @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if
* applicable). * 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 // check if this is a valid padId
if (!exports.isValidPadId(id)) { if (!exports.isValidPadId(id)) {
throw new CustomError(`${id} is not a valid padId`, 'apierror'); throw new CustomError(`${id} is not a valid padId`, 'apierror');

View file

@ -19,19 +19,21 @@
* limitations under the License. * limitations under the License.
*/ */
import {MapArrayType} from "../types/MapType";
const absolutePaths = require('../utils/AbsolutePaths'); const absolutePaths = require('../utils/AbsolutePaths');
const fs = require('fs'); import fs from 'fs';
const api = require('../db/API'); const api = require('../db/API');
const log4js = require('log4js'); import log4js from 'log4js';
const padManager = require('../db/PadManager'); const padManager = require('../db/PadManager');
const randomString = require('../utils/randomstring'); const randomString = require('../utils/randomstring');
const argv = require('../utils/Cli').argv; const argv = require('../utils/Cli').argv;
const createHTTPError = require('http-errors'); import createHTTPError from 'http-errors';
const apiHandlerLogger = log4js.getLogger('APIHandler'); const apiHandlerLogger = log4js.getLogger('APIHandler');
// ensure we have an apikey // ensure we have an apikey
let apikey = null; let apikey:string|null = null;
const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt'); const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt');
try { try {
@ -41,11 +43,11 @@ try {
apiHandlerLogger.info( apiHandlerLogger.info(
`Api key file "${apikeyFilename}" not found. Creating with random contents.`); `Api key file "${apikeyFilename}" not found. Creating with random contents.`);
apikey = randomString(32); apikey = randomString(32);
fs.writeFileSync(apikeyFilename, apikey, 'utf8'); fs.writeFileSync(apikeyFilename, apikey!, 'utf8');
} }
// a list of all functions // a list of all functions
const version = {}; const version:MapArrayType<any> = {};
version['1'] = { version['1'] = {
createGroup: [], createGroup: [],
@ -163,6 +165,14 @@ exports.latestApiVersion = '1.3.0';
// exports the versions so it can be used by the new Swagger endpoint // exports the versions so it can be used by the new Swagger endpoint
exports.version = version; exports.version = version;
type APIFields = {
apikey: string;
api_key: string;
padID: string;
padName: string;
}
/** /**
* Handles a HTTP API call * Handles a HTTP API call
* @param {String} apiVersion the version of the api * @param {String} apiVersion the version of the api
@ -171,7 +181,7 @@ exports.version = version;
* @req express request object * @req express request object
* @res express response 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 // say goodbye if this is an unknown API version
if (!(apiVersion in version)) { if (!(apiVersion in version)) {
throw new createHTTPError.NotFound('no such api version'); throw new createHTTPError.NotFound('no such api version');
@ -185,7 +195,7 @@ exports.handle = async function (apiVersion, functionName, fields) {
// check the api key! // check the api key!
fields.apikey = fields.apikey || fields.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'); 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 // put the function parameters in an array
// @ts-ignore
const functionParams = version[apiVersion][functionName].map((field) => fields[field]); const functionParams = version[apiVersion][functionName].map((field) => fields[field]);
// call the api function // call the api function

View file

@ -23,11 +23,11 @@
const exporthtml = require('../utils/ExportHtml'); const exporthtml = require('../utils/ExportHtml');
const exporttxt = require('../utils/ExportTxt'); const exporttxt = require('../utils/ExportTxt');
const exportEtherpad = require('../utils/ExportEtherpad'); const exportEtherpad = require('../utils/ExportEtherpad');
const fs = require('fs'); import fs from 'fs';
const settings = require('../utils/Settings'); const settings = require('../utils/Settings');
const os = require('os'); import os from 'os';
const hooks = require('../../static/js/pluginfw/hooks'); const hooks = require('../../static/js/pluginfw/hooks');
const util = require('util'); import util from 'util';
const { checkValidRev } = require('../utils/checkValidRev'); const { checkValidRev } = require('../utils/checkValidRev');
const fsp_writeFile = util.promisify(fs.writeFile); 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} readOnlyId the read only id of the pad to export
* @param {String} type the type 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 // avoid naming the read-only file as the original pad's id
let fileName = readOnlyId ? readOnlyId : padId; let fileName = readOnlyId ? readOnlyId : padId;

View file

@ -23,21 +23,22 @@
const padManager = require('../db/PadManager'); const padManager = require('../db/PadManager');
const padMessageHandler = require('./PadMessageHandler'); const padMessageHandler = require('./PadMessageHandler');
const fs = require('fs').promises; import {promises as fs} from 'fs';
const path = require('path'); import path from 'path';
const settings = require('../utils/Settings'); const settings = require('../utils/Settings');
const {Formidable} = require('formidable'); const {Formidable} = require('formidable');
const os = require('os'); import os from 'os';
const importHtml = require('../utils/ImportHtml'); const importHtml = require('../utils/ImportHtml');
const importEtherpad = require('../utils/ImportEtherpad'); const importEtherpad = require('../utils/ImportEtherpad');
const log4js = require('log4js'); import log4js from 'log4js';
const hooks = require('../../static/js/pluginfw/hooks.js'); const hooks = require('../../static/js/pluginfw/hooks.js');
const logger = log4js.getLogger('ImportHandler'); const logger = log4js.getLogger('ImportHandler');
// `status` must be a string supported by `importErrorMessage()` in `src/static/js/pad_impexp.js`. // `status` must be a string supported by `importErrorMessage()` in `src/static/js/pad_impexp.js`.
class ImportError extends Error { class ImportError extends Error {
constructor(status, ...args) { status: string;
constructor(status: string, ...args:any) {
super(...args); super(...args);
if (Error.captureStackTrace) Error.captureStackTrace(this, ImportError); if (Error.captureStackTrace) Error.captureStackTrace(this, ImportError);
this.name = 'ImportError'; this.name = 'ImportError';
@ -47,15 +48,15 @@ class ImportError extends Error {
} }
} }
const rm = async (path) => { const rm = async (path: string) => {
try { try {
await fs.unlink(path); await fs.unlink(path);
} catch (err) { } catch (err:any) {
if (err.code !== 'ENOENT') throw err; if (err.code !== 'ENOENT') throw err;
} }
}; };
let converter = null; let converter:any = null;
let exportExtension = 'htm'; let exportExtension = 'htm';
// load abiword only if it is enabled and if soffice is disabled // 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} padId the pad id to export
* @param {String} authorId the author id to use for the import * @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 // pipe to a file
// convert file to html via abiword or soffice // convert file to html via abiword or soffice
// set html in the pad // set html in the pad
@ -98,7 +99,7 @@ const doImport = async (req, res, padId, authorId) => {
let fields; let fields;
try { try {
[fields, files] = await form.parse(req); [fields, files] = await form.parse(req);
} catch (err) { } catch (err:any) {
logger.warn(`Import failed due to form error: ${err.stack || err}`); logger.warn(`Import failed due to form error: ${err.stack || err}`);
if (err.code === Formidable.formidableErrors.biggerThanMaxFileSize) { if (err.code === Formidable.formidableErrors.biggerThanMaxFileSize) {
throw new ImportError('maxFileSize'); 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 destFile = path.join(tmpDirectory, `etherpad_import_${randNum}.${exportExtension}`);
const context = {srcFile, destFile, fileEnding, padId, ImportError}; 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 fileIsEtherpad = (fileEnding === '.etherpad');
const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm'); const fileIsHTML = (fileEnding === '.html' || fileEnding === '.htm');
const fileIsTXT = (fileEnding === '.txt'); const fileIsTXT = (fileEnding === '.txt');
@ -169,7 +170,7 @@ const doImport = async (req, res, padId, authorId) => {
} else { } else {
try { try {
await converter.convertFile(srcFile, destFile, exportExtension); await converter.convertFile(srcFile, destFile, exportExtension);
} catch (err) { } catch (err:any) {
logger.warn(`Converting Error: ${err.stack || err}`); logger.warn(`Converting Error: ${err.stack || err}`);
throw new ImportError('convertFailed'); throw new ImportError('convertFailed');
} }
@ -210,7 +211,7 @@ const doImport = async (req, res, padId, authorId) => {
if (importHandledByPlugin || useConverter || fileIsHTML) { if (importHandledByPlugin || useConverter || fileIsHTML) {
try { try {
await importHtml.setPadHTML(pad, text, authorId); await importHtml.setPadHTML(pad, text, authorId);
} catch (err) { } catch (err:any) {
logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`); logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`);
} }
} else { } else {
@ -245,14 +246,14 @@ const doImport = async (req, res, padId, authorId) => {
* @param {String} authorId the author id to use for the import * @param {String} authorId the author id to use for the import
* @return {Promise<void>} a promise * @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 httpStatus = 200;
let code = 0; let code = 0;
let message = 'ok'; let message = 'ok';
let directDatabaseAccess; let directDatabaseAccess;
try { try {
directDatabaseAccess = await doImport(req, res, padId, authorId); directDatabaseAccess = await doImport(req, res, padId, authorId);
} catch (err) { } catch (err:any) {
const known = err instanceof ImportError && err.status; const known = err instanceof ImportError && err.status;
if (!known) logger.error(`Internal error during import: ${err.stack || err}`); if (!known) logger.error(`Internal error during import: ${err.stack || err}`);
httpStatus = known ? 400 : 500; httpStatus = known ? 400 : 500;

View file

@ -19,6 +19,8 @@
* limitations under the License. * limitations under the License.
*/ */
import {MapArrayType} from "../types/MapType";
const AttributeMap = require('../../static/js/AttributeMap'); const AttributeMap = require('../../static/js/AttributeMap');
const padManager = require('../db/PadManager'); const padManager = require('../db/PadManager');
const Changeset = require('../../static/js/Changeset'); const Changeset = require('../../static/js/Changeset');
@ -37,16 +39,19 @@ const accessLogger = log4js.getLogger('access');
const hooks = require('../../static/js/pluginfw/hooks.js'); const hooks = require('../../static/js/pluginfw/hooks.js');
const stats = require('../stats') const stats = require('../stats')
const assert = require('assert').strict; 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 webaccess = require('../hooks/express/webaccess');
const { checkValidRev } = require('../utils/checkValidRev'); const { checkValidRev } = require('../utils/checkValidRev');
let rateLimiter; let rateLimiter:any;
let socketio = null; let socketio:any = null;
hooks.deprecationNotices.clientReady = 'use the userJoin hook instead'; 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}); const newErr = new Error(`${pfx}${err.message}`, {cause: err});
if (Error.captureStackTrace) Error.captureStackTrace(newErr, addContextToError); if (Error.captureStackTrace) Error.captureStackTrace(newErr, addContextToError);
// Check for https://github.com/tc39/proposal-error-cause support, available in Node.js >= v16.10. // 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). * - readonly: Whether the client has read-only access (true) or read/write access (false).
* - rev: The last revision that was sent to the client. * - rev: The last revision that was sent to the client.
*/ */
const sessioninfos = {}; const sessioninfos:MapArrayType<any> = {};
exports.sessioninfos = sessioninfos; exports.sessioninfos = sessioninfos;
stats.gauge('totalUsers', () => socketio ? socketio.sockets.size : 0); stats.gauge('totalUsers', () => socketio ? socketio.sockets.size : 0);
@ -97,11 +102,13 @@ stats.gauge('activePads', () => {
* Processes one task at a time per channel. * Processes one task at a time per channel.
*/ */
class Channels { 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 * @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. * 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._exec = exec;
this._promiseChains = new Map(); this._promiseChains = new Map();
} }
@ -114,7 +121,7 @@ class Channels {
* @param {any} task - The task to give to the executor. * @param {any} task - The task to give to the executor.
* @returns {Promise<any>} The value returned by 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 p = (this._promiseChains.get(ch) || Promise.resolve()).then(() => this._exec(ch, task));
const pc = p const pc = p
.catch(() => {}) // Prevent rejections from halting the queue. .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 * This Method is called by server.ts to tell the message handler on which socket it should send
* @param socket_io The Socket * @param socket_io The Socket
*/ */
exports.setSocketIO = (socket_io) => { exports.setSocketIO = (socket_io:any) => {
socketio = socket_io; socketio = socket_io;
}; };
@ -144,7 +151,7 @@ exports.setSocketIO = (socket_io) => {
* Handles the connection of a new user * Handles the connection of a new user
* @param socket the socket.io Socket object for the new connection from the client * @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(); stats.meter('connects').mark();
// Initialize sessioninfos for this new session // Initialize sessioninfos for this new session
@ -154,7 +161,7 @@ exports.handleConnect = (socket) => {
/** /**
* Kicks all sessions from a pad * Kicks all sessions from a pad
*/ */
exports.kickSessionsFromPad = (padID) => { exports.kickSessionsFromPad = (padID: string) => {
if (typeof socketio.sockets.clients !== 'object') return; if (typeof socketio.sockets.clients !== 'object') return;
// skip if there is nobody on this pad // skip if there is nobody on this pad
@ -168,13 +175,13 @@ exports.kickSessionsFromPad = (padID) => {
* Handles the disconnection of a user * Handles the disconnection of a user
* @param socket the socket.io Socket object for the client * @param socket the socket.io Socket object for the client
*/ */
exports.handleDisconnect = async (socket) => { exports.handleDisconnect = async (socket:any) => {
stats.meter('disconnects').mark(); stats.meter('disconnects').mark();
const session = sessioninfos[socket.id]; const session = sessioninfos[socket.id];
delete sessioninfos[socket.id]; delete sessioninfos[socket.id];
// session.padId can be nullish if the user disconnects before sending CLIENT_READY. // session.padId can be nullish if the user disconnects before sending CLIENT_READY.
if (!session || !session.author || !session.padId) return; 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 */ /* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */
accessLogger.info('[LEAVE]' + accessLogger.info('[LEAVE]' +
` pad:${session.padId}` + ` pad:${session.padId}` +
@ -206,7 +213,7 @@ exports.handleDisconnect = async (socket) => {
* @param socket the socket.io Socket object for the client * @param socket the socket.io Socket object for the client
* @param message the message from 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'; const env = process.env.NODE_ENV || 'development';
if (env === 'production') { if (env === 'production') {
@ -263,7 +270,7 @@ exports.handleMessage = async (socket, message) => {
throw new Error(`pre-CLIENT_READY message from IP ${ip}: ${msg}`); 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} = const {accessStatus, authorID} =
await securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, user); await securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, user);
if (accessStatus !== 'grant') { 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. // 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; return;
} }
@ -376,7 +383,7 @@ exports.handleMessage = async (socket, message) => {
* @param socket the socket.io Socket object for the client * @param socket the socket.io Socket object for the client
* @param message the message from 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 {padId, author: authorId} = sessioninfos[socket.id];
const pad = await padManager.getPad(padId, null, authorId); const pad = await padManager.getPad(padId, null, authorId);
await pad.addSavedRevision(pad.head, 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 msg {Object} the message we're sending
* @param sessionID {string} the socketIO session to which we're sending this message * @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 (msg.data.type === 'CUSTOM') {
if (sessionID) { if (sessionID) {
// a sessionID is targeted: directly to this 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 padID {Pad} the pad to which we're sending this message
* @param msgString {String} the message we're sending * @param msgString {String} the message we're sending
*/ */
exports.handleCustomMessage = (padID, msgString) => { exports.handleCustomMessage = (padID: string, msgString:string) => {
const time = Date.now(); const time = Date.now();
const msg = { const msg = {
type: 'COLLABROOM', type: 'COLLABROOM',
@ -424,7 +431,7 @@ exports.handleCustomMessage = (padID, msgString) => {
* @param socket the socket.io Socket object for the client * @param socket the socket.io Socket object for the client
* @param message the message from 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 chatMessage = ChatMessage.fromObject(message.data.message);
const {padId, author: authorId} = sessioninfos[socket.id]; const {padId, author: authorId} = sessioninfos[socket.id];
// Don't trust the user-supplied values. // 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 * @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. * 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); const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt);
padId = mt instanceof ChatMessage ? puId : padId; padId = mt instanceof ChatMessage ? puId : padId;
const pad = await padManager.getPad(padId, null, message.authorId); 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 socket the socket.io Socket object for the client
* @param message the message from 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(start)) throw new Error(`missing or invalid start: ${start}`);
if (!Number.isInteger(end)) throw new Error(`missing or invalid end: ${end}`); if (!Number.isInteger(end)) throw new Error(`missing or invalid end: ${end}`);
const count = end - start; 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 socket the socket.io Socket object for the client
* @param message the message from 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; const {newName, unnamedId} = message.data.payload;
if (newName == null) throw new Error('missing newName'); if (newName == null) throw new Error('missing newName');
if (unnamedId == null) throw new Error('missing unnamedId'); 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 socket the socket.io Socket object for the client
* @param message the message from 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 (colorId == null) throw new Error('missing colorId');
if (!name) name = null; if (!name) name = null;
const session = sessioninfos[socket.id]; 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 socket the socket.io Socket object for the client
* @param message the message from 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 // This one's no longer pending, as we're gonna process it now
stats.counter('pendingEdits').dec(); stats.counter('pendingEdits').dec();
@ -658,7 +665,7 @@ const handleUserChanges = async (socket, message) => {
thisSession.rev = newRev; thisSession.rev = newRev;
if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev); if (newRev !== r) thisSession.time = await pad.getRevisionDate(newRev);
await exports.updatePadClients(pad); await exports.updatePadClients(pad);
} catch (err) { } catch (err:any) {
socket.emit('message', {disconnect: 'badChangeset'}); socket.emit('message', {disconnect: 'badChangeset'});
stats.meter('failedChangesets').mark(); stats.meter('failedChangesets').mark();
messageLogger.warn(`Failed to apply USER_CHANGES from author ${thisSession.author} ` + 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 // skip this if no-one is on this pad
const roomSockets = _getRoomSockets(pad.id); const roomSockets = _getRoomSockets(pad.id);
if (roomSockets.length === 0) return; if (roomSockets.length === 0) return;
@ -683,7 +690,7 @@ exports.updatePadClients = async (pad) => {
// via async.forEach with sequential for() loop. There is no real // via async.forEach with sequential for() loop. There is no real
// benefits of running this in parallel, // benefits of running this in parallel,
// but benefit of reusing cached revision object is HUGE // but benefit of reusing cached revision object is HUGE
const revCache = {}; const revCache:MapArrayType<any> = {};
await Promise.all(roomSockets.map(async (socket) => { await Promise.all(roomSockets.map(async (socket) => {
const sessioninfo = sessioninfos[socket.id]; const sessioninfo = sessioninfos[socket.id];
@ -717,7 +724,7 @@ exports.updatePadClients = async (pad) => {
}; };
try { try {
socket.emit('message', msg); socket.emit('message', msg);
} catch (err) { } catch (err:any) {
messageLogger.error(`Failed to notify user of new revision: ${err.stack || err}`); messageLogger.error(`Failed to notify user of new revision: ${err.stack || err}`);
return; return;
} }
@ -730,7 +737,7 @@ exports.updatePadClients = async (pad) => {
/** /**
* Copied from the Etherpad Source Code. Don't know what this method does excatly... * 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; const text = atext.text;
// collect char positions of line markers (e.g. bullets) in new atext // collect char positions of line markers (e.g. bullets) in new atext
@ -739,7 +746,7 @@ const _correctMarkersInPad = (atext, apool) => {
let offset = 0; let offset = 0;
for (const op of Changeset.deserializeOps(atext.attribs)) { for (const op of Changeset.deserializeOps(atext.attribs)) {
const attribs = AttributeMap.fromString(op.attribs, apool); 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) { if (hasMarker) {
for (let i = 0; i < op.chars; i++) { for (let i = 0; i < op.chars; i++) {
if (offset > 0 && text.charAt(offset - 1) !== '\n') { 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 socket the socket.io Socket object for the client
* @param message the message from 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]; const sessionInfo = sessioninfos[socket.id];
if (sessionInfo == null) throw new Error('client disconnected'); if (sessionInfo == null) throw new Error('client disconnected');
assert(sessionInfo.author); assert(sessionInfo.author);
@ -805,8 +812,11 @@ const handleClientReady = async (socket, message) => {
const currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber()); const currentTime = await pad.getRevisionDate(pad.getHeadRevisionNumber());
// get all author data out of the database (in parallel) // get all author data out of the database (in parallel)
const historicalAuthorData = {}; const historicalAuthorData:MapArrayType<{
await Promise.all(authors.map(async (authorId) => { name: string;
colorId: string;
}> = {};
await Promise.all(authors.map(async (authorId: string) => {
const author = await authorManager.getAuthor(authorId); const author = await authorManager.getAuthor(authorId);
if (!author) { if (!author) {
messageLogger.error(`There is no author for authorId: ${authorId}. ` + 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 */ /* eslint-disable prefer-template -- it doesn't support breaking across multiple lines */
accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` + accessLogger.info(`[${pad.head > 0 ? 'ENTER' : 'CREATE'}]` +
` pad:${sessionInfo.padId}` + ` pad:${sessionInfo.padId}` +
@ -859,7 +869,7 @@ const handleClientReady = async (socket, message) => {
// By using client revision, // By using client revision,
// this below code sends all the revisions missed during the client reconnect // this below code sends all the revisions missed during the client reconnect
const revisionsNeeded = []; const revisionsNeeded = [];
const changesets = {}; const changesets:MapArrayType<any> = {};
let startNum = message.client_rev + 1; let startNum = message.client_rev + 1;
let endNum = pad.getHeadRevisionNumber() + 1; let endNum = pad.getHeadRevisionNumber() + 1;
@ -919,7 +929,7 @@ const handleClientReady = async (socket, message) => {
const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
apool = attribsForWire.pool.toJsonable(); apool = attribsForWire.pool.toJsonable();
atext.attribs = attribsForWire.translated; atext.attribs = attribsForWire.translated;
} catch (e) { } catch (e:any) {
messageLogger.error(e.stack || e); messageLogger.error(e.stack || e);
socket.emit('message', {disconnect: 'corruptPad'}); // pull the brakes socket.emit('message', {disconnect: 'corruptPad'}); // pull the brakes
throw new Error('corrupt pad'); 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 // 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... // would open a security hole 1 swedish mile wide...
const clientVars = { const clientVars:MapArrayType<any> = {
skinName: settings.skinName, skinName: settings.skinName,
skinVariants: settings.skinVariants, skinVariants: settings.skinVariants,
randomVersionString: settings.randomVersionString, randomVersionString: settings.randomVersionString,
@ -1078,7 +1088,7 @@ const handleClientReady = async (socket, message) => {
/** /**
* Handles a request for a rough changeset, the timeslider client needs it * 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 (granularity == null) throw new Error('missing granularity');
if (!Number.isInteger(granularity)) throw new Error('granularity is not an integer'); if (!Number.isInteger(granularity)) throw new Error('granularity is not an integer');
if (start == null) throw new Error('missing start'); if (start == null) throw new Error('missing start');
@ -1090,7 +1100,7 @@ const handleChangesetRequest = async (socket, {data: {granularity, start, reques
const headRev = pad.getHeadRevisionNumber(); const headRev = pad.getHeadRevisionNumber();
if (start > headRev) if (start > headRev)
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; data.requestID = requestID;
socket.emit('message', {type: 'CHANGESET_REQ', data}); 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 * 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 * 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(); const headRevision = pad.getHeadRevisionNumber();
// calculate the last full endnum // calculate the last full endnum
@ -1124,8 +1134,8 @@ const getChangesetInfo = async (pad, startNum, endNum, granularity) => {
} }
// Get all needed db values in parallel. // Get all needed db values in parallel.
const composedChangesets = {}; const composedChangesets:MapArrayType<any> = {};
const revisionDate = []; const revisionDate:number[] = [];
const [lines] = await Promise.all([ const [lines] = await Promise.all([
getPadLines(pad, startNum - 1), getPadLines(pad, startNum - 1),
// Get all needed composite Changesets. // 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 * 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 * 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 // get the atext
let atext; let atext;
@ -1196,7 +1206,7 @@ const getPadLines = async (pad, revNum) => {
* Tries to rebuild the composePadChangeset function of the original Etherpad * 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 * 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 // fetch all changesets we need
const headNum = pad.getHeadRevisionNumber(); const headNum = pad.getHeadRevisionNumber();
endNum = Math.min(endNum, headNum + 1); endNum = Math.min(endNum, headNum + 1);
@ -1210,7 +1220,7 @@ const composePadChangesets = async (pad, startNum, endNum) => {
} }
// get all changesets // get all changesets
const changesets = {}; const changesets:MapArrayType<ChangeSet> = {};
await Promise.all(changesetsNeeded.map( await Promise.all(changesetsNeeded.map(
(revNum) => pad.getRevisionChangeset(revNum) (revNum) => pad.getRevisionChangeset(revNum)
.then((changeset) => changesets[revNum] = changeset))); .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. const ns = socketio.sockets; // Default namespace.
// We could call adapter.clients(), but that method is unnecessarily asynchronous. Replicate what // 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 // it does here, but synchronously to avoid a race condition. This code will have to change when
// we update to socket.io v3. // we update to socket.io v3.
const room = ns.adapter.rooms?.get(padID); const room = ns.adapter.rooms?.get(padID);
if (!room) return []; if (!room) return [];
return Array.from(room) return Array.from(room)
@ -1251,15 +1261,15 @@ const _getRoomSockets = (padID) => {
/** /**
* Get the number of users in a pad * Get the number of users in a pad
*/ */
exports.padUsersCount = (padID) => ({ exports.padUsersCount = (padID:string) => ({
padUsersCount: _getRoomSockets(padID).length, padUsersCount: _getRoomSockets(padID).length,
}); });
/** /**
* Get the list of users in a pad * Get the list of users in a pad
*/ */
exports.padUsers = async (padID) => { exports.padUsers = async (padID: string) => {
const padUsers = []; const padUsers:PadAuthor[] = [];
// iterate over all clients (in parallel) // iterate over all clients (in parallel)
await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => { await Promise.all(_getRoomSockets(padID).map(async (roomSocket) => {

View file

@ -20,6 +20,8 @@
* limitations under the License. * limitations under the License.
*/ */
import {MapArrayType} from "../types/MapType";
import {SocketModule} from "../types/SocketModule";
const log4js = require('log4js'); const log4js = require('log4js');
const settings = require('../utils/Settings'); const settings = require('../utils/Settings');
const stats = require('../../node/stats') const stats = require('../../node/stats')
@ -31,15 +33,15 @@ const logger = log4js.getLogger('socket.io');
* key is the component name * key is the component name
* value is the component module * value is the component module
*/ */
const components = {}; const components:MapArrayType<any> = {};
let io; let io:any;
/** adds a component /** adds a component
* @param {string} moduleName * @param {string} moduleName
* @param {Module} module * @param {Module} module
*/ */
exports.addComponent = (moduleName, module) => { exports.addComponent = (moduleName: string, module: SocketModule) => {
if (module == null) return exports.deleteComponent(moduleName); if (module == null) return exports.deleteComponent(moduleName);
components[moduleName] = module; components[moduleName] = module;
module.setSocketIO(io); module.setSocketIO(io);
@ -49,22 +51,22 @@ exports.addComponent = (moduleName, module) => {
* removes a component * removes a component
* @param {Module} moduleName * @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 * sets the socket.io and adds event functions for routing
* @param {Object} _io the socket.io instance * @param {Object} _io the socket.io instance
*/ */
exports.setSocketIO = (_io) => { exports.setSocketIO = (_io:any) => {
io = _io; io = _io;
io.sockets.on('connection', (socket) => { io.sockets.on('connection', (socket:any) => {
const ip = settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip; const ip = settings.disableIPlogging ? 'ANONYMOUS' : socket.request.ip;
logger.debug(`${socket.id} connected from IP ${ip}`); logger.debug(`${socket.id} connected from IP ${ip}`);
// wrap the original send function to log the messages // wrap the original send function to log the messages
socket._send = socket.send; socket._send = socket.send;
socket.send = (message) => { socket.send = (message: string) => {
logger.debug(`to ${socket.id}: ${JSON.stringify(message)}`); logger.debug(`to ${socket.id}: ${JSON.stringify(message)}`);
socket._send(message); socket._send(message);
}; };
@ -74,7 +76,7 @@ exports.setSocketIO = (_io) => {
components[i].handleConnect(socket); components[i].handleConnect(socket);
} }
socket.on('message', (message, ack = () => {}) => (async () => { socket.on('message', (message: any, ack: any = () => {}) => (async () => {
if (!message.component || !components[message.component]) { if (!message.component || !components[message.component]) {
throw new Error(`unknown message component: ${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. 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}`); logger.debug(`${socket.id} disconnected: ${reason}`);
// store the lastDisconnect as a timestamp, this is useful if you want to know // 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 // when the last user disconnected. If your activePads is 0 and totalUsers is 0

View file

@ -1,7 +1,10 @@
'use strict'; 'use strict';
const assert = require('assert').strict; import {strict as assert} from "assert";
const log4js = require('log4js'); 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 httpLogger = log4js.getLogger('http');
const settings = require('../../utils/Settings'); const settings = require('../../utils/Settings');
const hooks = require('../../../static/js/pluginfw/hooks'); 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'; hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
// Promisified wrapper around hooks.aCallFirst. // Promisified wrapper around hooks.aCallFirst.
const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, reject) => { const aCallFirst = (hookName: string, context:any, pred = null) => new Promise((resolve, reject) => {
hooks.aCallFirst(hookName, context, (err, r) => err != null ? reject(err) : resolve(r), pred); hooks.aCallFirst(hookName, context, (err:any, r: unknown) => err != null ? reject(err) : resolve(r), pred);
}); });
const aCallFirst0 = 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; if (!level) return false;
switch (level) { switch (level) {
case true: case true:
@ -32,7 +36,7 @@ exports.normalizeAuthzLevel = (level) => {
return false; return false;
}; };
exports.userCanModify = (padId, req) => { exports.userCanModify = (padId: string, req: SocketClientRequest) => {
if (readOnlyManager.isReadOnlyId(padId)) return false; if (readOnlyManager.isReadOnlyId(padId)) return false;
if (!settings.requireAuthentication) return true; if (!settings.requireAuthentication) return true;
const {session: {user} = {}} = req; const {session: {user} = {}} = req;
@ -45,7 +49,7 @@ exports.userCanModify = (padId, req) => {
// Exported so that tests can set this to 0 to avoid unnecessary test slowness. // Exported so that tests can set this to 0 to avoid unnecessary test slowness.
exports.authnFailureDelayMs = 1000; 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'); 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. // use the preAuthzFailure hook to override the default 403 error.
// /////////////////////////////////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////////////////////////////////
let results; let results: null|boolean[];
let skip = false; let skip = false;
const preAuthorizeNext = (...args) => { skip = true; next(...args); }; const preAuthorizeNext = (...args:any) => { skip = true; next(...args); };
try { try {
results = await aCallFirst('preAuthorize', {req, res, next: preAuthorizeNext}, results = await aCallFirst('preAuthorize', {req, res, next: preAuthorizeNext},
// This predicate will cause aCallFirst to call the hook functions one at a time until one // 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. // 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 // This prevents plugin authors from accidentally granting admin privileges to the general
// public. // public.
(r) => (skip || (r != null && r.filter((x) => (!requireAdmin || !x)).length > 0))); // @ts-ignore
} catch (err) { (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()}`); httpLogger.error(`Error in preAuthorize hook: ${err.stack || err.toString()}`);
if (!skip) res.status(500).send('Internal Server Error'); if (!skip) res.status(500).send('Internal Server Error');
return; 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 // 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). // authentication is checked and once after (if settings.requireAuthorization is true).
const authorize = async () => { const authorize = async () => {
const grant = async (level) => { const grant = async (level: string|false) => {
level = exports.normalizeAuthzLevel(level); level = exports.normalizeAuthzLevel(level);
if (!level) return false; if (!level) return false;
const user = req.session.user; const user = req.session.user;
@ -132,7 +137,7 @@ const checkAccess = async (req, res, next) => {
// /////////////////////////////////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////////////////////////////////
if (settings.users == null) settings.users = {}; 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 // If the HTTP basic auth header is present, extract the username and password so it can be given
// to authn plugins. // to authn plugins.
const httpBasicAuth = req.headers.authorization && req.headers.authorization.startsWith('Basic '); 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))) { if (!(await aCallFirst0('authenticate', ctx))) {
// Fall back to HTTP basic auth. // Fall back to HTTP basic auth.
const {[ctx.username]: {password} = {}} = settings.users; // @ts-ignore
const {[ctx.username]: {password} = {}} = settings.users as SettingsUser;
if (!httpBasicAuth || if (!httpBasicAuth ||
!ctx.username || !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 middleware to authenticate the user and check authorization. Must be installed after the
* express-session middleware. * 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))); 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>>, getAllAuthorColors: ()=>Promise<MapArrayType<string>>,
remove: ()=>Promise<void>, remove: ()=>Promise<void>,
text: ()=>string, text: ()=>string,
setText: (text: string)=>Promise<void>, setText: (text: string, authorId?: string)=>Promise<void>,
appendText: (text: 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'; 'use strict';
import {APool} from "../types/PadType";
/** /**
* 2014 John McLear (Etherpad Foundation / McLear Ltd) * 2014 John McLear (Etherpad Foundation / McLear Ltd)
* *
@ -22,17 +24,17 @@ const Stream = require('./Stream');
const authorManager = require('../db/AuthorManager'); const authorManager = require('../db/AuthorManager');
const db = require('../db/DB'); const db = require('../db/DB');
const hooks = require('../../static/js/pluginfw/hooks'); const hooks = require('../../static/js/pluginfw/hooks');
const log4js = require('log4js'); import log4js from 'log4js';
const supportedElems = require('../../static/js/contentcollector').supportedElems; const supportedElems = require('../../static/js/contentcollector').supportedElems;
const ueberdb = require('ueberdb2'); import ueberdb from 'ueberdb2';
const logger = log4js.getLogger('ImportEtherpad'); const logger = log4js.getLogger('ImportEtherpad');
exports.setPadRaw = async (padId, r, authorId = '') => { exports.setPadRaw = async (padId: string, r: string, authorId = '') => {
const records = JSON.parse(r); const records = JSON.parse(r);
// get supported block Elements from plugins, we will use this later. // 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); supportedElems.add(element);
}); });
@ -43,8 +45,8 @@ exports.setPadRaw = async (padId, r, authorId = '') => {
'pad', 'pad',
]; ];
let originalPadId = null; let originalPadId:string|null = null;
const checkOriginalPadId = (padId) => { const checkOriginalPadId = (padId: string) => {
if (originalPadId == null) originalPadId = padId; if (originalPadId == null) originalPadId = padId;
if (originalPadId !== padId) throw new Error('unexpected pad ID in record'); 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}); const padDb = new ueberdb.Database('memory', {data});
await padDb.init(); await padDb.init();
try { try {
const processRecord = async (key, value) => { const processRecord = async (key:string, value: null|{
padIDs: string|Record<string, unknown>,
pool: APool
}) => {
if (!value) return; if (!value) return;
const keyParts = key.split(':'); const keyParts = key.split(':');
const [prefix, id] = keyParts; const [prefix, id] = keyParts;
@ -79,7 +84,7 @@ exports.setPadRaw = async (padId, r, authorId = '') => {
if (prefix === 'pad' && keyParts.length === 2) { if (prefix === 'pad' && keyParts.length === 2) {
const pool = new AttributePool().fromJsonable(value.pool); const pool = new AttributePool().fromJsonable(value.pool);
const unsupportedElements = new Set(); const unsupportedElements = new Set();
pool.eachAttrib((k, v) => { pool.eachAttrib((k: string, v:any) => {
if (!supportedElems.has(k)) unsupportedElements.add(k); if (!supportedElems.has(k)) unsupportedElements.add(k);
}); });
if (unsupportedElements.size) { if (unsupportedElements.size) {
@ -94,8 +99,10 @@ exports.setPadRaw = async (padId, r, authorId = '') => {
`importEtherpad hook function processes it: ${key}`); `importEtherpad hook function processes it: ${key}`);
return; return;
} }
// @ts-ignore
await padDb.set(key, value); await padDb.set(key, value);
}; };
// @ts-ignore
const readOps = new Stream(Object.entries(records)).map(([k, v]) => processRecord(k, v)); const readOps = new Stream(Object.entries(records)).map(([k, v]) => processRecord(k, v));
for (const op of readOps.batch(100).buffer(99)) await op; for (const op of readOps.batch(100).buffer(99)) await op;

View file

@ -15,15 +15,16 @@
* limitations under the License. * limitations under the License.
*/ */
const log4js = require('log4js'); import log4js from 'log4js';
const Changeset = require('../../static/js/Changeset'); const Changeset = require('../../static/js/Changeset');
const contentcollector = require('../../static/js/contentcollector'); const contentcollector = require('../../static/js/contentcollector');
const jsdom = require('jsdom'); import jsdom from 'jsdom';
import {PadType} from "../types/PadType";
const apiLogger = log4js.getLogger('ImportHtml'); 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) { if (processor == null) {
const [{rehype}, {default: minifyWhitespace}] = const [{rehype}, {default: minifyWhitespace}] =
await Promise.all([import('rehype'), import('rehype-minify-whitespace')]); await Promise.all([import('rehype'), import('rehype-minify-whitespace')]);
@ -46,7 +47,7 @@ exports.setPadHTML = async (pad, html, authorId = '') => {
try { try {
// we use a try here because if the HTML is bad it will blow up // we use a try here because if the HTML is bad it will blow up
cc.collectContent(document.body); cc.collectContent(document.body);
} catch (err) { } catch (err: any) {
apiLogger.warn(`Error processing HTML: ${err.stack || err}`); apiLogger.warn(`Error processing HTML: ${err.stack || err}`);
throw err; throw err;
} }

View file

@ -81,6 +81,8 @@
"devDependencies": { "devDependencies": {
"@types/async": "^3.2.24", "@types/async": "^3.2.24",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/http-errors": "^2.0.4",
"@types/jsdom": "^21.1.6",
"@types/mocha": "^10.0.6", "@types/mocha": "^10.0.6",
"@types/node": "^20.11.19", "@types/node": "^20.11.19",
"@types/sinon": "^17.0.3", "@types/sinon": "^17.0.3",

View file

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */ /* Visit https://aka.ms/tsconfig to read more about this file */
"moduleDetection": "force", "moduleDetection": "force",
"lib": ["es6"], "lib": ["ES2023"],
/* Language and Environment */ /* Language and Environment */
"target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
/* Modules */ /* Modules */