mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-23 00:46:16 -04:00
Added typescript to etherpad
* Fixed determining file extension. * Added ts-node * Fixed backend tests. * Fixed frontend test runs. * Fixed tests. * Use script approach for starting etherpad. * Change directory to src. * Fixed env. * Change directory * Fixed build arg. * Fixed docker build. * Fixed. * Fixed cypress file path. * Fixed. * Use latest node container. * Fixed windows workflow. * Use tsx and optimized docker image. * Added workflow for type checks. * Fixed. * Added tsconfig. * Converted more files to typescript. * Removed commented keys. * Typed caching middleware. * Added script for checking the types. * Moved SecretRotator to typescript. * Fixed npm installation and moved to types folder. * Use better scripts for watching typescript changes. * Update windows.yml * Fixed order of npm installation. * Converted i18n. * Added more types. * Added more types. * Fixed import. * Fixed tests. * Fixed tests. * Fixed type checking test. * Fixed stats * Added express types. * fixed.
This commit is contained in:
parent
c3202284bc
commit
ead3c0ea38
74 changed files with 1259 additions and 612 deletions
|
@ -10,4 +10,4 @@ Module file names start with a capital letter and uses camelCase
|
|||
|
||||
# Where does it start?
|
||||
|
||||
server.js is started directly
|
||||
server.ts is started directly
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
const ueberDB = require('ueberdb2');
|
||||
const settings = require('../utils/Settings');
|
||||
const log4js = require('log4js');
|
||||
const stats = require('../stats');
|
||||
const stats = require('../stats')
|
||||
|
||||
const logger = log4js.getLogger('ueberDB');
|
||||
|
||||
|
@ -47,13 +47,13 @@ exports.init = async () => {
|
|||
}
|
||||
for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) {
|
||||
const f = exports.db[fn];
|
||||
exports[fn] = async (...args) => await f.call(exports.db, ...args);
|
||||
exports[fn] = async (...args:string[]) => await f.call(exports.db, ...args);
|
||||
Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f));
|
||||
Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f));
|
||||
}
|
||||
};
|
||||
|
||||
exports.shutdown = async (hookName, context) => {
|
||||
exports.shutdown = async (hookName: string, context:any) => {
|
||||
if (exports.db != null) await exports.db.close();
|
||||
exports.db = null;
|
||||
logger.log('Database closed');
|
|
@ -34,9 +34,10 @@ class SessionStore extends Store {
|
|||
for (const {timeout} of this._expirations.values()) clearTimeout(timeout);
|
||||
}
|
||||
|
||||
async _updateExpirations(sid, sess, updateDbExp = true) {
|
||||
async _updateExpirations(sid: string, sess: any, updateDbExp = true) {
|
||||
const exp = this._expirations.get(sid) || {};
|
||||
clearTimeout(exp.timeout);
|
||||
// @ts-ignore
|
||||
const {cookie: {expires} = {}} = sess || {};
|
||||
if (expires) {
|
||||
const sessExp = new Date(expires).getTime();
|
||||
|
@ -63,23 +64,23 @@ class SessionStore extends Store {
|
|||
return sess;
|
||||
}
|
||||
|
||||
async _write(sid, sess) {
|
||||
async _write(sid: string, sess: any) {
|
||||
await DB.set(`sessionstorage:${sid}`, sess);
|
||||
}
|
||||
|
||||
async _get(sid) {
|
||||
async _get(sid: string) {
|
||||
logger.debug(`GET ${sid}`);
|
||||
const s = await DB.get(`sessionstorage:${sid}`);
|
||||
return await this._updateExpirations(sid, s);
|
||||
}
|
||||
|
||||
async _set(sid, sess) {
|
||||
async _set(sid: string, sess:any) {
|
||||
logger.debug(`SET ${sid}`);
|
||||
sess = await this._updateExpirations(sid, sess);
|
||||
if (sess != null) await this._write(sid, sess);
|
||||
}
|
||||
|
||||
async _destroy(sid) {
|
||||
async _destroy(sid:string) {
|
||||
logger.debug(`DESTROY ${sid}`);
|
||||
clearTimeout((this._expirations.get(sid) || {}).timeout);
|
||||
this._expirations.delete(sid);
|
||||
|
@ -89,7 +90,7 @@ class SessionStore extends Store {
|
|||
// Note: express-session might call touch() before it calls set() for the first time. Ideally this
|
||||
// would behave like set() in that case but it's OK if it doesn't -- express-session will call
|
||||
// set() soon enough.
|
||||
async _touch(sid, sess) {
|
||||
async _touch(sid: string, sess:any) {
|
||||
logger.debug(`TOUCH ${sid}`);
|
||||
sess = await this._updateExpirations(sid, sess, false);
|
||||
if (sess == null) return; // Already expired.
|
|
@ -35,7 +35,7 @@ const log4js = require('log4js');
|
|||
const messageLogger = log4js.getLogger('message');
|
||||
const accessLogger = log4js.getLogger('access');
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const stats = require('../stats');
|
||||
const stats = require('../stats')
|
||||
const assert = require('assert').strict;
|
||||
const {RateLimiterMemory} = require('rate-limiter-flexible');
|
||||
const webaccess = require('../hooks/express/webaccess');
|
||||
|
@ -133,7 +133,7 @@ class Channels {
|
|||
const padChannels = new Channels((ch, {socket, message}) => handleUserChanges(socket, message));
|
||||
|
||||
/**
|
||||
* This Method is called by server.js 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
|
||||
*/
|
||||
exports.setSocketIO = (socket_io) => {
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
const log4js = require('log4js');
|
||||
const settings = require('../utils/Settings');
|
||||
const stats = require('../stats');
|
||||
const stats = require('../../node/stats')
|
||||
|
||||
const logger = log4js.getLogger('socket.io');
|
||||
|
||||
|
|
|
@ -1,25 +1,31 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('underscore');
|
||||
const SecretRotator = require('../security/SecretRotator');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const events = require('events');
|
||||
const express = require('express');
|
||||
const expressSession = require('express-session');
|
||||
const fs = require('fs');
|
||||
import {Socket} from "node:net";
|
||||
import type {MapArrayType} from "../types/MapType";
|
||||
|
||||
import _ from 'underscore';
|
||||
// @ts-ignore
|
||||
import cookieParser from 'cookie-parser';
|
||||
import events from 'events';
|
||||
import express from 'express';
|
||||
// @ts-ignore
|
||||
import expressSession from 'express-session';
|
||||
import fs from 'fs';
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const log4js = require('log4js');
|
||||
import log4js from 'log4js';
|
||||
const SessionStore = require('../db/SessionStore');
|
||||
const settings = require('../utils/Settings');
|
||||
const stats = require('../stats');
|
||||
const util = require('util');
|
||||
const stats = require('../stats')
|
||||
import util from 'util';
|
||||
const webaccess = require('./express/webaccess');
|
||||
|
||||
let secretRotator = null;
|
||||
import SecretRotator from '../security/SecretRotator';
|
||||
|
||||
let secretRotator: SecretRotator|null = null;
|
||||
const logger = log4js.getLogger('http');
|
||||
let serverName;
|
||||
let sessionStore;
|
||||
const sockets = new Set();
|
||||
let serverName:string;
|
||||
let sessionStore: { shutdown: () => void; } | null;
|
||||
const sockets:Set<Socket> = new Set();
|
||||
const socketsEvents = new events.EventEmitter();
|
||||
const startTime = stats.settableGauge('httpStartTime');
|
||||
|
||||
|
@ -101,7 +107,7 @@ exports.restartServer = async () => {
|
|||
console.log(`SSL -- server key file: ${settings.ssl.key}`);
|
||||
console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`);
|
||||
|
||||
const options = {
|
||||
const options: MapArrayType<any> = {
|
||||
key: fs.readFileSync(settings.ssl.key),
|
||||
cert: fs.readFileSync(settings.ssl.cert),
|
||||
};
|
||||
|
@ -163,7 +169,7 @@ exports.restartServer = async () => {
|
|||
app.use((req, res, next) => {
|
||||
const stopWatch = stats.timer('httpRequests').start();
|
||||
const sendFn = res.send.bind(res);
|
||||
res.send = (...args) => { stopWatch.end(); sendFn(...args); };
|
||||
res.send = (...args) => { stopWatch.end(); return sendFn(...args); };
|
||||
next();
|
||||
});
|
||||
|
||||
|
@ -173,7 +179,7 @@ exports.restartServer = async () => {
|
|||
// anyway.
|
||||
if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) {
|
||||
app.use(log4js.connectLogger(logger, {
|
||||
level: log4js.levels.DEBUG,
|
||||
level: log4js.levels.DEBUG.levelStr,
|
||||
format: ':status, :method :url',
|
||||
}));
|
||||
}
|
||||
|
@ -237,7 +243,7 @@ exports.restartServer = async () => {
|
|||
hooks.aCallAll('expressConfigure', {app}),
|
||||
hooks.aCallAll('expressCreateServer', {app, server: exports.server}),
|
||||
]);
|
||||
exports.server.on('connection', (socket) => {
|
||||
exports.server.on('connection', (socket:Socket) => {
|
||||
sockets.add(socket);
|
||||
socketsEvents.emit('updated');
|
||||
socket.on('close', () => {
|
||||
|
@ -250,6 +256,6 @@ exports.restartServer = async () => {
|
|||
logger.info('HTTP server listening for connections');
|
||||
};
|
||||
|
||||
exports.shutdown = async (hookName, context) => {
|
||||
exports.shutdown = async (hookName:string, context: any) => {
|
||||
await closeServer();
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
const eejs = require('../../eejs');
|
||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
||||
|
||||
const eejs = require('../../eejs');
|
||||
|
||||
/**
|
||||
* Add the admin navigation link
|
||||
|
@ -9,8 +10,8 @@ const eejs = require('../../eejs');
|
|||
* @param {Function} cb the callback function
|
||||
* @return {*}
|
||||
*/
|
||||
exports.expressCreateServer = (hookName, args, cb) => {
|
||||
args.app.get('/admin', (req, res) => {
|
||||
exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function): any => {
|
||||
args.app.get('/admin', (req:any, res:any) => {
|
||||
if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/');
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/admin/index.html', {req}));
|
||||
});
|
|
@ -1,5 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
||||
import {Socket} from "node:net";
|
||||
import {ErrorCaused} from "../../types/ErrorCaused";
|
||||
import {QueryType} from "../../types/QueryType";
|
||||
import {PluginType} from "../../types/Plugin";
|
||||
|
||||
const eejs = require('../../eejs');
|
||||
const settings = require('../../utils/Settings');
|
||||
const installer = require('../../../static/js/pluginfw/installer');
|
||||
|
@ -8,8 +14,8 @@ const plugins = require('../../../static/js/pluginfw/plugins');
|
|||
const semver = require('semver');
|
||||
const UpdateCheck = require('../../utils/UpdateCheck');
|
||||
|
||||
exports.expressCreateServer = (hookName, args, cb) => {
|
||||
args.app.get('/admin/plugins', (req, res) => {
|
||||
exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function) => {
|
||||
args.app.get('/admin/plugins', (req:any, res:any) => {
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins.html', {
|
||||
plugins: pluginDefs.plugins,
|
||||
req,
|
||||
|
@ -17,7 +23,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
}));
|
||||
});
|
||||
|
||||
args.app.get('/admin/plugins/info', (req, res) => {
|
||||
args.app.get('/admin/plugins/info', (req:any, res:any) => {
|
||||
const gitCommit = settings.getGitCommit();
|
||||
const epVersion = settings.getEpVersion();
|
||||
|
||||
|
@ -36,13 +42,14 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
return cb();
|
||||
};
|
||||
|
||||
exports.socketio = (hookName, args, cb) => {
|
||||
exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
||||
const io = args.io.of('/pluginfw/installer');
|
||||
io.on('connection', (socket) => {
|
||||
io.on('connection', (socket:any) => {
|
||||
// @ts-ignore
|
||||
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
|
||||
if (!isAdmin) return;
|
||||
|
||||
socket.on('getInstalled', (query) => {
|
||||
socket.on('getInstalled', (query:string) => {
|
||||
// send currently installed plugins
|
||||
const installed =
|
||||
Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package);
|
||||
|
@ -66,13 +73,14 @@ exports.socketio = (hookName, args, cb) => {
|
|||
|
||||
socket.emit('results:updatable', {updatable});
|
||||
} catch (err) {
|
||||
console.warn(err.stack || err.toString());
|
||||
const errc = err as ErrorCaused
|
||||
console.warn(errc.stack || errc.toString());
|
||||
|
||||
socket.emit('results:updatable', {updatable: {}});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('getAvailable', async (query) => {
|
||||
socket.on('getAvailable', async (query:string) => {
|
||||
try {
|
||||
const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ false);
|
||||
socket.emit('results:available', results);
|
||||
|
@ -82,7 +90,7 @@ exports.socketio = (hookName, args, cb) => {
|
|||
}
|
||||
});
|
||||
|
||||
socket.on('search', async (query) => {
|
||||
socket.on('search', async (query: QueryType) => {
|
||||
try {
|
||||
const results = await installer.search(query.searchTerm, /* maxCacheAge:*/ 60 * 10);
|
||||
let res = Object.keys(results)
|
||||
|
@ -98,8 +106,8 @@ exports.socketio = (hookName, args, cb) => {
|
|||
}
|
||||
});
|
||||
|
||||
socket.on('install', (pluginName) => {
|
||||
installer.install(pluginName, (err) => {
|
||||
socket.on('install', (pluginName: string) => {
|
||||
installer.install(pluginName, (err: ErrorCaused) => {
|
||||
if (err) console.warn(err.stack || err.toString());
|
||||
|
||||
socket.emit('finished:install', {
|
||||
|
@ -110,8 +118,8 @@ exports.socketio = (hookName, args, cb) => {
|
|||
});
|
||||
});
|
||||
|
||||
socket.on('uninstall', (pluginName) => {
|
||||
installer.uninstall(pluginName, (err) => {
|
||||
socket.on('uninstall', (pluginName:string) => {
|
||||
installer.uninstall(pluginName, (err:ErrorCaused) => {
|
||||
if (err) console.warn(err.stack || err.toString());
|
||||
|
||||
socket.emit('finished:uninstall', {plugin: pluginName, error: err ? err.message : null});
|
||||
|
@ -128,11 +136,13 @@ exports.socketio = (hookName, args, cb) => {
|
|||
* @param {String} dir The directory of the plugin
|
||||
* @return {Object[]}
|
||||
*/
|
||||
const sortPluginList = (plugins, property, /* ASC?*/dir) => plugins.sort((a, b) => {
|
||||
const sortPluginList = (plugins:PluginType[], property:string, /* ASC?*/dir:string): object[] => plugins.sort((a, b) => {
|
||||
// @ts-ignore
|
||||
if (a[property] < b[property]) {
|
||||
return dir ? -1 : 1;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (a[property] > b[property]) {
|
||||
return dir ? 1 : -1;
|
||||
}
|
|
@ -6,8 +6,8 @@ const hooks = require('../../../static/js/pluginfw/hooks');
|
|||
const plugins = require('../../../static/js/pluginfw/plugins');
|
||||
const settings = require('../../utils/Settings');
|
||||
|
||||
exports.expressCreateServer = (hookName, {app}) => {
|
||||
app.get('/admin/settings', (req, res) => {
|
||||
exports.expressCreateServer = (hookName:string, {app}:any) => {
|
||||
app.get('/admin/settings', (req:any, res:any) => {
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', {
|
||||
req,
|
||||
settings: '',
|
||||
|
@ -16,12 +16,13 @@ exports.expressCreateServer = (hookName, {app}) => {
|
|||
});
|
||||
};
|
||||
|
||||
exports.socketio = (hookName, {io}) => {
|
||||
io.of('/settings').on('connection', (socket) => {
|
||||
exports.socketio = (hookName:string, {io}:any) => {
|
||||
io.of('/settings').on('connection', (socket: any ) => {
|
||||
// @ts-ignore
|
||||
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
|
||||
if (!isAdmin) return;
|
||||
|
||||
socket.on('load', async (query) => {
|
||||
socket.on('load', async (query:string):Promise<any> => {
|
||||
let data;
|
||||
try {
|
||||
data = await fsp.readFile(settings.settingsFilename, 'utf8');
|
||||
|
@ -36,7 +37,7 @@ exports.socketio = (hookName, {io}) => {
|
|||
}
|
||||
});
|
||||
|
||||
socket.on('saveSettings', async (newSettings) => {
|
||||
socket.on('saveSettings', async (newSettings:string) => {
|
||||
await fsp.writeFile(settings.settingsFilename, newSettings);
|
||||
socket.emit('saveprogress', 'saved');
|
||||
});
|
|
@ -6,15 +6,15 @@ const {Formidable} = require('formidable');
|
|||
const apiHandler = require('../../handler/APIHandler');
|
||||
const util = require('util');
|
||||
|
||||
exports.expressPreSession = async (hookName, {app}) => {
|
||||
exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||
// The Etherpad client side sends information about how a disconnect happened
|
||||
app.post('/ep/pad/connection-diagnostic-info', async (req, res) => {
|
||||
app.post('/ep/pad/connection-diagnostic-info', async (req:any, res:any) => {
|
||||
const [fields, files] = await (new Formidable({})).parse(req);
|
||||
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
|
||||
res.end('OK');
|
||||
});
|
||||
|
||||
const parseJserrorForm = async (req) => {
|
||||
const parseJserrorForm = async (req:any) => {
|
||||
const form = new Formidable({
|
||||
maxFileSize: 1, // Files are not expected. Not sure if 0 means unlimited, so 1 is used.
|
||||
});
|
||||
|
@ -23,11 +23,11 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
};
|
||||
|
||||
// The Etherpad client side sends information about client side javscript errors
|
||||
app.post('/jserror', (req, res, next) => {
|
||||
app.post('/jserror', (req:any, res:any, next:Function) => {
|
||||
(async () => {
|
||||
const data = JSON.parse(await parseJserrorForm(req));
|
||||
clientLogger.warn(`${data.msg} --`, {
|
||||
[util.inspect.custom]: (depth, options) => {
|
||||
[util.inspect.custom]: (depth: number, options:any) => {
|
||||
// Depth is forced to infinity to ensure that all of the provided data is logged.
|
||||
options = Object.assign({}, options, {depth: Infinity, colors: true});
|
||||
return util.inspect(data, options);
|
||||
|
@ -38,7 +38,7 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
});
|
||||
|
||||
// Provide a possibility to query the latest available API version
|
||||
app.get('/api', (req, res) => {
|
||||
app.get('/api', (req:any, res:any) => {
|
||||
res.json({currentVersion: apiHandler.latestApiVersion});
|
||||
});
|
||||
};
|
|
@ -1,12 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
const stats = require('../../stats');
|
||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
||||
import {ErrorCaused} from "../../types/ErrorCaused";
|
||||
|
||||
exports.expressCreateServer = (hook_name, args, cb) => {
|
||||
const stats = require('../../stats')
|
||||
|
||||
exports.expressCreateServer = (hook_name:string, args: ArgsExpressType, cb:Function) => {
|
||||
exports.app = args.app;
|
||||
|
||||
// Handle errors
|
||||
args.app.use((err, req, res, next) => {
|
||||
args.app.use((err:ErrorCaused, req:any, res:any, next:Function) => {
|
||||
// if an error occurs Connect will pass it down
|
||||
// through these "error-handling" middleware
|
||||
// allowing you to respond however you like
|
|
@ -1,5 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
||||
|
||||
const hasPadAccess = require('../../padaccess');
|
||||
const settings = require('../../utils/Settings');
|
||||
const exportHandler = require('../../handler/ExportHandler');
|
||||
|
@ -10,10 +12,10 @@ const rateLimit = require('express-rate-limit');
|
|||
const securityManager = require('../../db/SecurityManager');
|
||||
const webaccess = require('./webaccess');
|
||||
|
||||
exports.expressCreateServer = (hookName, args, cb) => {
|
||||
exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
||||
const limiter = rateLimit({
|
||||
...settings.importExportRateLimiting,
|
||||
handler: (request, response, next, options) => {
|
||||
handler: (request:any) => {
|
||||
if (request.rateLimit.current === request.rateLimit.limit + 1) {
|
||||
// when the rate limiter triggers, write a warning in the logs
|
||||
console.warn('Import/Export rate limiter triggered on ' +
|
||||
|
@ -24,7 +26,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
|
||||
// handle export requests
|
||||
args.app.use('/p/:pad/:rev?/export/:type', limiter);
|
||||
args.app.get('/p/:pad/:rev?/export/:type', (req, res, next) => {
|
||||
args.app.get('/p/:pad/:rev?/export/:type', (req:any, res:any, next:Function) => {
|
||||
(async () => {
|
||||
const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad'];
|
||||
// send a 404 if we don't support this filetype
|
||||
|
@ -70,8 +72,9 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
|
||||
// handle import requests
|
||||
args.app.use('/p/:pad/import', limiter);
|
||||
args.app.post('/p/:pad/import', (req, res, next) => {
|
||||
args.app.post('/p/:pad/import', (req:any, res:any, next:Function) => {
|
||||
(async () => {
|
||||
// @ts-ignore
|
||||
const {session: {user} = {}} = req;
|
||||
const {accessStatus, authorID: authorId} = await securityManager.checkAccess(
|
||||
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
|
|
@ -1,5 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
import {OpenAPIOperations, OpenAPISuccessResponse, SwaggerUIResource} from "../../types/SwaggerUIResource";
|
||||
import {MapArrayType} from "../../types/MapType";
|
||||
import {ErrorCaused} from "../../types/ErrorCaused";
|
||||
|
||||
/**
|
||||
* node/hooks/express/openapi.js
|
||||
*
|
||||
|
@ -52,8 +56,9 @@ const APIPathStyle = {
|
|||
REST: 'rest', // restful paths e.g. /rest/group/create
|
||||
};
|
||||
|
||||
|
||||
// API resources - describe your API endpoints here
|
||||
const resources = {
|
||||
const resources:SwaggerUIResource = {
|
||||
// Group
|
||||
group: {
|
||||
create: {
|
||||
|
@ -372,7 +377,7 @@ const defaultResponses = {
|
|||
},
|
||||
};
|
||||
|
||||
const defaultResponseRefs = {
|
||||
const defaultResponseRefs:OpenAPISuccessResponse = {
|
||||
200: {
|
||||
$ref: '#/components/responses/Success',
|
||||
},
|
||||
|
@ -388,16 +393,16 @@ const defaultResponseRefs = {
|
|||
};
|
||||
|
||||
// convert to a dictionary of operation objects
|
||||
const operations = {};
|
||||
const operations: OpenAPIOperations = {};
|
||||
for (const [resource, actions] of Object.entries(resources)) {
|
||||
for (const [action, spec] of Object.entries(actions)) {
|
||||
const {operationId, responseSchema, ...operation} = spec;
|
||||
const {operationId,responseSchema, ...operation} = spec;
|
||||
|
||||
// add response objects
|
||||
const responses = {...defaultResponseRefs};
|
||||
const responses:OpenAPISuccessResponse = {...defaultResponseRefs};
|
||||
if (responseSchema) {
|
||||
responses[200] = cloneDeep(defaultResponses.Success);
|
||||
responses[200].content['application/json'].schema.properties.data = {
|
||||
responses[200].content!['application/json'].schema.properties.data = {
|
||||
type: 'object',
|
||||
properties: responseSchema,
|
||||
};
|
||||
|
@ -414,7 +419,7 @@ for (const [resource, actions] of Object.entries(resources)) {
|
|||
}
|
||||
}
|
||||
|
||||
const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
|
||||
const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT) => {
|
||||
const definition = {
|
||||
openapi: OPENAPI_VERSION,
|
||||
info,
|
||||
|
@ -490,7 +495,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
|
|||
|
||||
// build operations
|
||||
for (const funcName of Object.keys(apiHandler.version[version])) {
|
||||
let operation = {};
|
||||
let operation:OpenAPIOperations = {};
|
||||
if (operations[funcName]) {
|
||||
operation = {...operations[funcName]};
|
||||
} else {
|
||||
|
@ -505,7 +510,9 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
|
|||
operation.parameters = operation.parameters || [];
|
||||
for (const paramName of apiHandler.version[version][funcName]) {
|
||||
operation.parameters.push({$ref: `#/components/parameters/${paramName}`});
|
||||
// @ts-ignore
|
||||
if (!definition.components.parameters[paramName]) {
|
||||
// @ts-ignore
|
||||
definition.components.parameters[paramName] = {
|
||||
name: paramName,
|
||||
in: 'query',
|
||||
|
@ -525,6 +532,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
|
|||
|
||||
// add to definition
|
||||
// NOTE: It may be confusing that every operation can be called with both GET and POST
|
||||
// @ts-ignore
|
||||
definition.paths[path] = {
|
||||
get: {
|
||||
...operation,
|
||||
|
@ -539,7 +547,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
|
|||
return definition;
|
||||
};
|
||||
|
||||
exports.expressPreSession = async (hookName, {app}) => {
|
||||
exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||
// create openapi-backend handlers for each api version under /api/{version}/*
|
||||
for (const version of Object.keys(apiHandler.version)) {
|
||||
// we support two different styles of api: flat + rest
|
||||
|
@ -552,7 +560,7 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
const definition = generateDefinitionForVersion(version, style);
|
||||
|
||||
// serve version specific openapi definition
|
||||
app.get(`${apiRoot}/openapi.json`, (req, res) => {
|
||||
app.get(`${apiRoot}/openapi.json`, (req:any, res:any) => {
|
||||
// For openapi definitions, wide CORS is probably fine
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]});
|
||||
|
@ -561,7 +569,7 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
// serve latest openapi definition file under /api/openapi.json
|
||||
const isLatestAPIVersion = version === apiHandler.latestApiVersion;
|
||||
if (isLatestAPIVersion) {
|
||||
app.get(`/${style}/openapi.json`, (req, res) => {
|
||||
app.get(`/${style}/openapi.json`, (req:any, res:any) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]});
|
||||
});
|
||||
|
@ -588,12 +596,12 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
|
||||
// register operation handlers
|
||||
for (const funcName of Object.keys(apiHandler.version[version])) {
|
||||
const handler = async (c, req, res) => {
|
||||
const handler = async (c: any, req:any, res:any) => {
|
||||
// parse fields from request
|
||||
const {header, params, query} = c.request;
|
||||
|
||||
// read form data if method was POST
|
||||
let formData = {};
|
||||
let formData:MapArrayType<any> = {};
|
||||
if (c.request.method === 'post') {
|
||||
const form = new IncomingForm();
|
||||
formData = (await form.parse(req))[0];
|
||||
|
@ -615,18 +623,19 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
try {
|
||||
data = await apiHandler.handle(version, funcName, fields, req, res);
|
||||
} catch (err) {
|
||||
const errCaused = err as ErrorCaused
|
||||
// convert all errors to http errors
|
||||
if (createHTTPError.isHttpError(err)) {
|
||||
// pass http errors thrown by handler forward
|
||||
throw err;
|
||||
} else if (err.name === 'apierror') {
|
||||
} else if (errCaused.name === 'apierror') {
|
||||
// parameters were wrong and the api stopped execution, pass the error
|
||||
// convert to http error
|
||||
throw new createHTTPError.BadRequest(err.message);
|
||||
throw new createHTTPError.BadRequest(errCaused.message);
|
||||
} else {
|
||||
// an unknown error happened
|
||||
// log it and throw internal error
|
||||
logger.error(err.stack || err.toString());
|
||||
logger.error(errCaused.stack || errCaused.toString());
|
||||
throw new createHTTPError.InternalError('internal error');
|
||||
}
|
||||
}
|
||||
|
@ -649,7 +658,7 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
|
||||
// start and bind to express
|
||||
api.init();
|
||||
app.use(apiRoot, async (req, res) => {
|
||||
app.use(apiRoot, async (req:any, res:any) => {
|
||||
let response = null;
|
||||
try {
|
||||
if (style === APIPathStyle.REST) {
|
||||
|
@ -660,31 +669,33 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
// pass to openapi-backend handler
|
||||
response = await api.handleRequest(req, req, res);
|
||||
} catch (err) {
|
||||
const errCaused = err as ErrorCaused
|
||||
// handle http errors
|
||||
res.statusCode = err.statusCode || 500;
|
||||
// @ts-ignore
|
||||
res.statusCode = errCaused.statusCode || 500;
|
||||
|
||||
// convert to our json response format
|
||||
// https://github.com/ether/etherpad-lite/tree/master/doc/api/http_api.md#response-format
|
||||
switch (res.statusCode) {
|
||||
case 403: // forbidden
|
||||
response = {code: 4, message: err.message, data: null};
|
||||
response = {code: 4, message: errCaused.message, data: null};
|
||||
break;
|
||||
case 401: // unauthorized (no or wrong api key)
|
||||
response = {code: 4, message: err.message, data: null};
|
||||
response = {code: 4, message: errCaused.message, data: null};
|
||||
break;
|
||||
case 404: // not found (no such function)
|
||||
response = {code: 3, message: err.message, data: null};
|
||||
response = {code: 3, message: errCaused.message, data: null};
|
||||
break;
|
||||
case 500: // server error (internal error)
|
||||
response = {code: 2, message: err.message, data: null};
|
||||
response = {code: 2, message: errCaused.message, data: null};
|
||||
break;
|
||||
case 400: // bad request (wrong parameters)
|
||||
// respond with 200 OK to keep old behavior and pass tests
|
||||
res.statusCode = 200; // @TODO: this is bad api design
|
||||
response = {code: 1, message: err.message, data: null};
|
||||
response = {code: 1, message: errCaused.message, data: null};
|
||||
break;
|
||||
default:
|
||||
response = {code: 1, message: err.message, data: null};
|
||||
response = {code: 1, message: errCaused.message, data: null};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -702,7 +713,7 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
* @param {APIPathStyle} style The style of the API path
|
||||
* @return {String} The root path for the API version
|
||||
*/
|
||||
const getApiRootForVersion = (version, style = APIPathStyle.FLAT) => `/${style}/${version}`;
|
||||
const getApiRootForVersion = (version:string, style:any = APIPathStyle.FLAT): string => `/${style}/${version}`;
|
||||
|
||||
/**
|
||||
* Helper to generate an OpenAPI server object when serving definitions
|
||||
|
@ -710,6 +721,8 @@ const getApiRootForVersion = (version, style = APIPathStyle.FLAT) => `/${style}/
|
|||
* @param {Request} req The express request object
|
||||
* @return {url: String} The server object for the OpenAPI definition location
|
||||
*/
|
||||
const generateServerForApiVersion = (apiRoot, req) => ({
|
||||
const generateServerForApiVersion = (apiRoot:string, req:any): {
|
||||
url:string
|
||||
} => ({
|
||||
url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`,
|
||||
});
|
|
@ -1,10 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
||||
|
||||
const padManager = require('../../db/PadManager');
|
||||
|
||||
exports.expressCreateServer = (hookName, args, cb) => {
|
||||
exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
||||
// redirects browser to the pad's sanitized url if needed. otherwise, renders the html
|
||||
args.app.param('pad', (req, res, next, padId) => {
|
||||
args.app.param('pad', (req:any, res:any, next:Function, padId:string) => {
|
||||
(async () => {
|
||||
// ensure the padname is valid and the url doesn't end with a /
|
||||
if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) {
|
|
@ -1,5 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
||||
|
||||
const events = require('events');
|
||||
const express = require('../express');
|
||||
const log4js = require('log4js');
|
||||
|
@ -10,7 +12,7 @@ const socketIORouter = require('../../handler/SocketIORouter');
|
|||
const hooks = require('../../../static/js/pluginfw/hooks');
|
||||
const padMessageHandler = require('../../handler/PadMessageHandler');
|
||||
|
||||
let io;
|
||||
let io:any;
|
||||
const logger = log4js.getLogger('socket.io');
|
||||
const sockets = new Set();
|
||||
const socketsEvents = new events.EventEmitter();
|
||||
|
@ -46,7 +48,7 @@ exports.expressCloseServer = async () => {
|
|||
logger.info('All socket.io clients have disconnected');
|
||||
};
|
||||
|
||||
exports.expressCreateServer = (hookName, args, cb) => {
|
||||
exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
||||
// init socket.io and redirect all requests to the MessageHandler
|
||||
// there shouldn't be a browser that isn't compatible to all
|
||||
// transports in this list at once
|
||||
|
@ -77,7 +79,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
|
||||
});
|
||||
|
||||
io.on('connect', (socket) => {
|
||||
io.on('connect', (socket:any) => {
|
||||
sockets.add(socket);
|
||||
socketsEvents.emit('updated');
|
||||
socket.on('disconnect', () => {
|
||||
|
@ -86,7 +88,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
});
|
||||
});
|
||||
|
||||
io.use((socket, next) => {
|
||||
io.use((socket:any, next: Function) => {
|
||||
const req = socket.request;
|
||||
// Express sets req.ip but socket.io does not. Replicate Express's behavior here.
|
||||
if (req.ip == null) {
|
||||
|
@ -105,8 +107,8 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
express.sessionMiddleware(req, {}, next);
|
||||
});
|
||||
|
||||
io.use((socket, next) => {
|
||||
socket.conn.on('packet', (packet) => {
|
||||
io.use((socket:any, next:Function) => {
|
||||
socket.conn.on('packet', (packet:string) => {
|
||||
// Tell express-session that the session is still active. The session store can use these
|
||||
// touch events to defer automatic session cleanup, and if express-session is configured with
|
||||
// rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not
|
|
@ -1,5 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
import type {MapArrayType} from "../types/MapType";
|
||||
import {I18nPluginDefs} from "../types/I18nPluginDefs";
|
||||
|
||||
const languages = require('languages4translatewiki');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
@ -11,17 +14,17 @@ const settings = require('../utils/Settings');
|
|||
// returns all existing messages merged together and grouped by langcode
|
||||
// {es: {"foo": "string"}, en:...}
|
||||
const getAllLocales = () => {
|
||||
const locales2paths = {};
|
||||
const locales2paths:MapArrayType<string[]> = {};
|
||||
|
||||
// Puts the paths of all locale files contained in a given directory
|
||||
// into `locales2paths` (files from various dirs are grouped by lang code)
|
||||
// (only json files with valid language code as name)
|
||||
const extractLangs = (dir) => {
|
||||
const extractLangs = (dir: string) => {
|
||||
if (!existsSync(dir)) return;
|
||||
let stat = fs.lstatSync(dir);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) return;
|
||||
|
||||
fs.readdirSync(dir).forEach((file) => {
|
||||
fs.readdirSync(dir).forEach((file:string) => {
|
||||
file = path.resolve(dir, file);
|
||||
stat = fs.lstatSync(file);
|
||||
if (stat.isDirectory() || stat.isSymbolicLink()) return;
|
||||
|
@ -40,15 +43,15 @@ const getAllLocales = () => {
|
|||
extractLangs(path.join(settings.root, 'src/locales'));
|
||||
|
||||
// add plugins languages (if any)
|
||||
for (const {package: {path: pluginPath}} of Object.values(pluginDefs.plugins)) {
|
||||
for (const {package: {path: pluginPath}} of Object.values<I18nPluginDefs>(pluginDefs.plugins)) {
|
||||
// plugin locales should overwrite etherpad's core locales
|
||||
if (pluginPath.endsWith('/ep_etherpad-lite') === true) continue;
|
||||
if (pluginPath.endsWith('/ep_etherpad-lite')) continue;
|
||||
extractLangs(path.join(pluginPath, 'locales'));
|
||||
}
|
||||
|
||||
// Build a locale index (merge all locale data other than user-supplied overrides)
|
||||
const locales = {};
|
||||
_.each(locales2paths, (files, langcode) => {
|
||||
const locales:MapArrayType<any> = {};
|
||||
_.each(locales2paths, (files: string[], langcode: string) => {
|
||||
locales[langcode] = {};
|
||||
|
||||
files.forEach((file) => {
|
||||
|
@ -70,9 +73,9 @@ const getAllLocales = () => {
|
|||
'for Customization for Administrators, under Localization.');
|
||||
if (settings.customLocaleStrings) {
|
||||
if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr;
|
||||
_.each(settings.customLocaleStrings, (overrides, langcode) => {
|
||||
_.each(settings.customLocaleStrings, (overrides:MapArrayType<string> , langcode:string) => {
|
||||
if (typeof overrides !== 'object') throw wrongFormatErr;
|
||||
_.each(overrides, (localeString, key) => {
|
||||
_.each(overrides, (localeString:string|object, key:string) => {
|
||||
if (typeof localeString !== 'string') throw wrongFormatErr;
|
||||
const locale = locales[langcode];
|
||||
|
||||
|
@ -102,8 +105,8 @@ const getAllLocales = () => {
|
|||
|
||||
// returns a hash of all available languages availables with nativeName and direction
|
||||
// e.g. { es: {nativeName: "español", direction: "ltr"}, ... }
|
||||
const getAvailableLangs = (locales) => {
|
||||
const result = {};
|
||||
const getAvailableLangs = (locales:MapArrayType<any>) => {
|
||||
const result:MapArrayType<string> = {};
|
||||
for (const langcode of Object.keys(locales)) {
|
||||
result[langcode] = languages.getLanguageInfo(langcode);
|
||||
}
|
||||
|
@ -111,7 +114,7 @@ const getAvailableLangs = (locales) => {
|
|||
};
|
||||
|
||||
// returns locale index that will be served in /locales.json
|
||||
const generateLocaleIndex = (locales) => {
|
||||
const generateLocaleIndex = (locales:MapArrayType<string>) => {
|
||||
const result = _.clone(locales); // keep English strings
|
||||
for (const langcode of Object.keys(locales)) {
|
||||
if (langcode !== 'en') result[langcode] = `locales/${langcode}.json`;
|
||||
|
@ -120,13 +123,13 @@ const generateLocaleIndex = (locales) => {
|
|||
};
|
||||
|
||||
|
||||
exports.expressPreSession = async (hookName, {app}) => {
|
||||
exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||
// regenerate locales on server restart
|
||||
const locales = getAllLocales();
|
||||
const localeIndex = generateLocaleIndex(locales);
|
||||
exports.availableLangs = getAvailableLangs(locales);
|
||||
|
||||
app.get('/locales/:locale', (req, res) => {
|
||||
app.get('/locales/:locale', (req:any, res:any) => {
|
||||
// works with /locale/en and /locale/en.json requests
|
||||
const locale = req.params.locale.split('.')[0];
|
||||
if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) {
|
||||
|
@ -138,7 +141,7 @@ exports.expressPreSession = async (hookName, {app}) => {
|
|||
}
|
||||
});
|
||||
|
||||
app.get('/locales.json', (req, res) => {
|
||||
app.get('/locales.json', (req: any, res:any) => {
|
||||
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.send(localeIndex);
|
|
@ -2,7 +2,7 @@
|
|||
const securityManager = require('./db/SecurityManager');
|
||||
|
||||
// checks for padAccess
|
||||
module.exports = async (req, res) => {
|
||||
module.exports = async (req: { params?: any; cookies?: any; session?: any; }, res: { status: (arg0: number) => { (): any; new(): any; send: { (arg0: string): void; new(): any; }; }; }) => {
|
||||
const {session: {user} = {}} = req;
|
||||
const accessObj = await securityManager.checkAccess(
|
||||
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
|
|
@ -1,4 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
|
||||
import {DeriveModel} from "../types/DeriveModel";
|
||||
import {LegacyParams} from "../types/LegacyParams";
|
||||
|
||||
const {Buffer} = require('buffer');
|
||||
const crypto = require('./crypto');
|
||||
|
@ -6,22 +9,24 @@ const db = require('../db/DB');
|
|||
const log4js = require('log4js');
|
||||
|
||||
class Kdf {
|
||||
async generateParams() { throw new Error('not implemented'); }
|
||||
async derive(params, info) { throw new Error('not implemented'); }
|
||||
async generateParams(): Promise<{ salt: string; digest: string; keyLen: number; secret: string }> { throw new Error('not implemented'); }
|
||||
async derive(params: DeriveModel, info: any) { throw new Error('not implemented'); }
|
||||
}
|
||||
|
||||
class LegacyStaticSecret extends Kdf {
|
||||
async derive(params, info) { return params; }
|
||||
async derive(params:any, info:any) { return params; }
|
||||
}
|
||||
|
||||
class Hkdf extends Kdf {
|
||||
constructor(digest, keyLen) {
|
||||
private readonly _digest: string
|
||||
private readonly _keyLen: number
|
||||
constructor(digest:string, keyLen:number) {
|
||||
super();
|
||||
this._digest = digest;
|
||||
this._keyLen = keyLen;
|
||||
}
|
||||
|
||||
async generateParams() {
|
||||
async generateParams(): Promise<{ salt: string; digest: string; keyLen: number; secret: string }> {
|
||||
const [secret, salt] = (await Promise.all([
|
||||
crypto.randomBytes(this._keyLen),
|
||||
crypto.randomBytes(this._keyLen),
|
||||
|
@ -29,7 +34,7 @@ class Hkdf extends Kdf {
|
|||
return {digest: this._digest, keyLen: this._keyLen, salt, secret};
|
||||
}
|
||||
|
||||
async derive(p, info) {
|
||||
async derive(p: DeriveModel, info:any) {
|
||||
return Buffer.from(
|
||||
await crypto.hkdf(p.digest, p.secret, p.salt, info, p.keyLen)).toString('hex');
|
||||
}
|
||||
|
@ -48,8 +53,8 @@ const algorithms = [
|
|||
const defaultAlgId = algorithms.length - 1;
|
||||
|
||||
// In JavaScript, the % operator is remainder, not modulus.
|
||||
const mod = (a, n) => ((a % n) + n) % n;
|
||||
const intervalStart = (t, interval) => t - mod(t, interval);
|
||||
const mod = (a:number, n:number) => ((a % n) + n) % n;
|
||||
const intervalStart = (t:number, interval:number) => t - mod(t, interval);
|
||||
|
||||
/**
|
||||
* Maintains an array of secrets across one or more Etherpad instances sharing the same database,
|
||||
|
@ -58,7 +63,15 @@ const intervalStart = (t, interval) => t - mod(t, interval);
|
|||
* The secrets are generated using a key derivation function (KDF) with input keying material coming
|
||||
* from a long-lived secret stored in the database (generated if missing).
|
||||
*/
|
||||
class SecretRotator {
|
||||
export class SecretRotator {
|
||||
readonly secrets: string[];
|
||||
private readonly _dbPrefix
|
||||
private readonly _interval
|
||||
private readonly _legacyStaticSecret
|
||||
private readonly _lifetime
|
||||
private readonly _logger
|
||||
private _updateTimeout:any
|
||||
private readonly _t
|
||||
/**
|
||||
* @param {string} dbPrefix - Database key prefix to use for tracking secret metadata.
|
||||
* @param {number} interval - How often to rotate in a new secret.
|
||||
|
@ -68,7 +81,7 @@ class SecretRotator {
|
|||
* rotation. If the oldest known secret starts after `lifetime` ago, this secret will cover
|
||||
* the time period starting `lifetime` ago and ending at the start of that secret.
|
||||
*/
|
||||
constructor(dbPrefix, interval, lifetime, legacyStaticSecret = null) {
|
||||
constructor(dbPrefix: string, interval: number, lifetime: number, legacyStaticSecret:string|null = null) {
|
||||
/**
|
||||
* The secrets. The first secret in this array is the one that should be used to generate new
|
||||
* MACs. All of the secrets in this array should be used when attempting to authenticate an
|
||||
|
@ -94,7 +107,7 @@ class SecretRotator {
|
|||
this._t = {now: Date.now.bind(Date), setTimeout, clearTimeout, algorithms};
|
||||
}
|
||||
|
||||
async _publish(params, id = null) {
|
||||
async _publish(params: LegacyParams, id:string|null = null) {
|
||||
// Params are published to the db with a randomly generated key to avoid race conditions with
|
||||
// other instances.
|
||||
if (id == null) id = `${this._dbPrefix}:${(await crypto.randomBytes(32)).toString('hex')}`;
|
||||
|
@ -114,7 +127,7 @@ class SecretRotator {
|
|||
this._updateTimeout = null;
|
||||
}
|
||||
|
||||
async _deriveSecrets(p, now) {
|
||||
async _deriveSecrets(p: any, now: number) {
|
||||
this._logger.debug('deriving secrets from', p);
|
||||
if (!p.interval) return [await algorithms[p.algId].derive(p.algParams, null)];
|
||||
const t0 = intervalStart(now, p.interval);
|
||||
|
@ -139,7 +152,7 @@ class SecretRotator {
|
|||
// Whether the derived secret for the interval starting at tN is still relevant. If there was no
|
||||
// clock skew, a derived secret is relevant until p.lifetime has elapsed since the end of the
|
||||
// interval. To accommodate clock skew, this end time is extended by p.interval.
|
||||
const expired = (tN) => now >= tN + (2 * p.interval) + p.lifetime;
|
||||
const expired = (tN:number) => now >= tN + (2 * p.interval) + p.lifetime;
|
||||
// Walk from t0 back until either the start of coverage or the derived secret is expired. t0
|
||||
// must always be the first entry in case p is the current params. (The first derived secret is
|
||||
// used for generating MACs, so the secret derived for t0 must be before the secrets derived for
|
||||
|
@ -160,12 +173,12 @@ class SecretRotator {
|
|||
// TODO: This is racy. If two instances start up at the same time and there are no existing
|
||||
// matching publications, each will generate and publish their own paramters. In practice this
|
||||
// is unlikely to happen, and if it does it can be fixed by restarting both Etherpad instances.
|
||||
const dbKeys = await db.findKeys(`${this._dbPrefix}:*`, null) || [];
|
||||
let currentParams = null;
|
||||
const dbKeys:string[] = await db.findKeys(`${this._dbPrefix}:*`, null) || [];
|
||||
let currentParams:any = null;
|
||||
let currentId = null;
|
||||
const dbWrites = [];
|
||||
const dbWrites:any[] = [];
|
||||
const allParams = [];
|
||||
const legacyParams = [];
|
||||
const legacyParams:LegacyParams[] = [];
|
||||
await Promise.all(dbKeys.map(async (dbKey) => {
|
||||
const p = await db.get(dbKey);
|
||||
if (p.algId === 0 && p.algParams === this._legacyStaticSecret) legacyParams.push(p);
|
||||
|
@ -198,7 +211,7 @@ class SecretRotator {
|
|||
!legacyParams.find((p) => p.end + p.lifetime >= legacyEnd + this._lifetime)) {
|
||||
const d = new Date(legacyEnd).toJSON();
|
||||
this._logger.debug(`adding legacy static secret for ${d} with lifetime ${this._lifetime}`);
|
||||
const p = {
|
||||
const p: LegacyParams = {
|
||||
algId: 0,
|
||||
algParams: this._legacyStaticSecret,
|
||||
// The start time is equal to the end time so that this legacy secret does not affect the
|
||||
|
@ -248,4 +261,4 @@ class SecretRotator {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = SecretRotator;
|
||||
export default SecretRotator
|
|
@ -24,11 +24,15 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const log4js = require('log4js');
|
||||
import {PluginType} from "./types/Plugin";
|
||||
import {ErrorCaused} from "./types/ErrorCaused";
|
||||
import {PromiseHooks} from "node:v8";
|
||||
|
||||
import log4js from 'log4js';
|
||||
|
||||
const settings = require('./utils/Settings');
|
||||
|
||||
let wtfnode;
|
||||
let wtfnode: any;
|
||||
if (settings.dumpOnUncleanExit) {
|
||||
// wtfnode should be loaded after log4js.replaceConsole() so that it uses log4js for logging, and
|
||||
// it should be above everything else so that it can hook in before resources are used.
|
||||
|
@ -51,7 +55,7 @@ const pluginDefs = require('../static/js/pluginfw/plugin_defs');
|
|||
const plugins = require('../static/js/pluginfw/plugins');
|
||||
const installer = require('../static/js/pluginfw/installer');
|
||||
const {Gate} = require('./utils/promises');
|
||||
const stats = require('./stats');
|
||||
const stats = require('./stats')
|
||||
|
||||
const logger = log4js.getLogger('server');
|
||||
|
||||
|
@ -68,14 +72,15 @@ const State = {
|
|||
|
||||
let state = State.INITIAL;
|
||||
|
||||
const removeSignalListener = (signal, listener) => {
|
||||
const removeSignalListener = (signal: NodeJS.Signals, listener: NodeJS.SignalsListener) => {
|
||||
logger.debug(`Removing ${signal} listener because it might interfere with shutdown tasks. ` +
|
||||
`Function code:\n${listener.toString()}\n` +
|
||||
`Current stack:\n${(new Error()).stack.split('\n').slice(1).join('\n')}`);
|
||||
`Current stack:\n${new Error()!.stack!.split('\n').slice(1).join('\n')}`);
|
||||
process.off(signal, listener);
|
||||
};
|
||||
|
||||
let startDoneGate;
|
||||
|
||||
let startDoneGate: { resolve: () => void; }
|
||||
exports.start = async () => {
|
||||
switch (state) {
|
||||
case State.INITIAL:
|
||||
|
@ -102,15 +107,17 @@ exports.start = async () => {
|
|||
// Check if Etherpad version is up-to-date
|
||||
UpdateCheck.check();
|
||||
|
||||
// @ts-ignore
|
||||
stats.gauge('memoryUsage', () => process.memoryUsage().rss);
|
||||
// @ts-ignore
|
||||
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
process.on('uncaughtException', (err: ErrorCaused) => {
|
||||
logger.debug(`uncaught exception: ${err.stack || err}`);
|
||||
|
||||
// eslint-disable-next-line promise/no-promise-in-callback
|
||||
exports.exit(err)
|
||||
.catch((err) => {
|
||||
.catch((err: ErrorCaused) => {
|
||||
logger.error('Error in process exit', err);
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit(1);
|
||||
|
@ -118,12 +125,12 @@ exports.start = async () => {
|
|||
});
|
||||
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
|
||||
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
|
||||
process.on('unhandledRejection', (err) => {
|
||||
process.on('unhandledRejection', (err: ErrorCaused) => {
|
||||
logger.debug(`unhandled rejection: ${err.stack || err}`);
|
||||
throw err;
|
||||
});
|
||||
|
||||
for (const signal of ['SIGINT', 'SIGTERM']) {
|
||||
for (const signal of ['SIGINT', 'SIGTERM'] as NodeJS.Signals[]) {
|
||||
// Forcibly remove other signal listeners to prevent them from terminating node before we are
|
||||
// done cleaning up. See https://github.com/andywer/threads.js/pull/329 for an example of a
|
||||
// problematic listener. This means that exports.exit is solely responsible for performing all
|
||||
|
@ -142,7 +149,7 @@ exports.start = async () => {
|
|||
await db.init();
|
||||
await installer.checkForMigration();
|
||||
await plugins.update();
|
||||
const installedPlugins = Object.values(pluginDefs.plugins)
|
||||
const installedPlugins = (Object.values(pluginDefs.plugins) as PluginType[])
|
||||
.filter((plugin) => plugin.package.name !== 'ep_etherpad-lite')
|
||||
.map((plugin) => `${plugin.package.name}@${plugin.package.version}`)
|
||||
.join(', ');
|
||||
|
@ -190,7 +197,7 @@ exports.stop = async () => {
|
|||
logger.info('Stopping Etherpad...');
|
||||
state = State.STOPPING;
|
||||
try {
|
||||
let timeout = null;
|
||||
let timeout: NodeJS.Timeout = null as unknown as NodeJS.Timeout;
|
||||
await Promise.race([
|
||||
hooks.aCallAll('shutdown'),
|
||||
new Promise((resolve, reject) => {
|
||||
|
@ -209,15 +216,15 @@ exports.stop = async () => {
|
|||
stopDoneGate.resolve();
|
||||
};
|
||||
|
||||
let exitGate;
|
||||
let exitGate: any;
|
||||
let exitCalled = false;
|
||||
exports.exit = async (err = null) => {
|
||||
exports.exit = async (err: ErrorCaused|string|null = null) => {
|
||||
/* eslint-disable no-process-exit */
|
||||
if (err === 'SIGTERM') {
|
||||
// Termination from SIGTERM is not treated as an abnormal termination.
|
||||
logger.info('Received SIGTERM signal');
|
||||
err = null;
|
||||
} else if (err != null) {
|
||||
} else if (typeof err == "object" && err != null) {
|
||||
logger.error(`Metrics at time of fatal error:\n${JSON.stringify(stats.toJSON(), null, 2)}`);
|
||||
logger.error(err.stack || err.toString());
|
||||
process.exitCode = 1;
|
||||
|
@ -277,4 +284,6 @@ exports.exit = async (err = null) => {
|
|||
};
|
||||
|
||||
if (require.main === module) exports.start();
|
||||
|
||||
// @ts-ignore
|
||||
if (typeof(PhusionPassenger) !== 'undefined') exports.start();
|
|
@ -4,6 +4,7 @@ const measured = require('measured-core');
|
|||
|
||||
module.exports = measured.createCollection();
|
||||
|
||||
// @ts-ignore
|
||||
module.exports.shutdown = async (hookName, context) => {
|
||||
module.exports.end();
|
||||
};
|
||||
};
|
5
src/node/types/ArgsExpressType.ts
Normal file
5
src/node/types/ArgsExpressType.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type ArgsExpressType = {
|
||||
app:any,
|
||||
io: any,
|
||||
server:any
|
||||
}
|
5
src/node/types/AsyncQueueTask.ts
Normal file
5
src/node/types/AsyncQueueTask.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type AsyncQueueTask = {
|
||||
srcFile: string,
|
||||
destFile: string,
|
||||
type: string
|
||||
}
|
6
src/node/types/DeriveModel.ts
Normal file
6
src/node/types/DeriveModel.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export type DeriveModel = {
|
||||
digest: string,
|
||||
secret: string,
|
||||
salt: string,
|
||||
keyLen: number
|
||||
}
|
14
src/node/types/ErrorCaused.ts
Normal file
14
src/node/types/ErrorCaused.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export class ErrorCaused extends Error {
|
||||
cause: Error;
|
||||
code: any;
|
||||
constructor(message: string, cause: Error) {
|
||||
super();
|
||||
this.cause = cause
|
||||
this.name = "ErrorCaused"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type ErrorCause = {
|
||||
|
||||
}
|
5
src/node/types/I18nPluginDefs.ts
Normal file
5
src/node/types/I18nPluginDefs.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type I18nPluginDefs = {
|
||||
package: {
|
||||
path: string
|
||||
}
|
||||
}
|
8
src/node/types/LegacyParams.ts
Normal file
8
src/node/types/LegacyParams.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export type LegacyParams = {
|
||||
start: number,
|
||||
end: number,
|
||||
lifetime: number,
|
||||
algId: number,
|
||||
algParams: any,
|
||||
interval:number|null
|
||||
}
|
7
src/node/types/MapType.ts
Normal file
7
src/node/types/MapType.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export type MapType = {
|
||||
[key: string|number]: string|number
|
||||
}
|
||||
|
||||
export type MapArrayType<T> = {
|
||||
[key:string]: T
|
||||
}
|
16
src/node/types/PadType.ts
Normal file
16
src/node/types/PadType.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
export type PadType = {
|
||||
apool: ()=>APool,
|
||||
atext: AText,
|
||||
getInternalRevisionAText: (text:string)=>Promise<AText>
|
||||
}
|
||||
|
||||
|
||||
type APool = {
|
||||
putAttrib: ([],flag: boolean)=>number
|
||||
}
|
||||
|
||||
|
||||
export type AText = {
|
||||
text: string,
|
||||
attribs: any
|
||||
}
|
9
src/node/types/Plugin.ts
Normal file
9
src/node/types/Plugin.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
|
||||
export type PluginType = {
|
||||
package: {
|
||||
name: string,
|
||||
version: string
|
||||
}
|
||||
}
|
8
src/node/types/PromiseWithStd.ts
Normal file
8
src/node/types/PromiseWithStd.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import type {Readable} from "node:stream";
|
||||
import type {ChildProcess} from "node:child_process";
|
||||
|
||||
export type PromiseWithStd = {
|
||||
stdout?: Readable|null,
|
||||
stderr?: Readable|null,
|
||||
child?: ChildProcess
|
||||
} & Promise<any>
|
3
src/node/types/QueryType.ts
Normal file
3
src/node/types/QueryType.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export type QueryType = {
|
||||
searchTerm: string; sortBy: string; sortDir: string; offset: number; limit: number;
|
||||
}
|
15
src/node/types/RunCMDOptions.ts
Normal file
15
src/node/types/RunCMDOptions.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export type RunCMDOptions = {
|
||||
cwd?: string,
|
||||
stdio?: string[],
|
||||
env?: NodeJS.ProcessEnv
|
||||
}
|
||||
|
||||
export type RunCMDPromise = {
|
||||
stdout?:Function,
|
||||
stderr?:Function
|
||||
}
|
||||
|
||||
export type ErrorExtended = {
|
||||
code?: number|null,
|
||||
signal?: NodeJS.Signals|null
|
||||
}
|
3
src/node/types/SecretRotatorType.ts
Normal file
3
src/node/types/SecretRotatorType.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export type SecretRotatorType = {
|
||||
stop: ()=>void
|
||||
}
|
34
src/node/types/SwaggerUIResource.ts
Normal file
34
src/node/types/SwaggerUIResource.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
export type SwaggerUIResource = {
|
||||
[key: string]: {
|
||||
[secondKey: string]: {
|
||||
operationId: string,
|
||||
summary?: string,
|
||||
description?:string
|
||||
responseSchema?: object
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type OpenAPISuccessResponse = {
|
||||
[key: number] :{
|
||||
$ref: string,
|
||||
content?: {
|
||||
[key: string]: {
|
||||
schema: {
|
||||
properties: {
|
||||
data: {
|
||||
type: string,
|
||||
properties: object
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type OpenAPIOperations = {
|
||||
[key:string]: any
|
||||
}
|
|
@ -19,6 +19,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {ChildProcess} from "node:child_process";
|
||||
import {AsyncQueueTask} from "../types/AsyncQueueTask";
|
||||
|
||||
const spawn = require('child_process').spawn;
|
||||
const async = require('async');
|
||||
const settings = require('./Settings');
|
||||
|
@ -27,13 +30,13 @@ const os = require('os');
|
|||
// on windows we have to spawn a process for each convertion,
|
||||
// cause the plugin abicommand doesn't exist on this platform
|
||||
if (os.type().indexOf('Windows') > -1) {
|
||||
exports.convertFile = async (srcFile, destFile, type) => {
|
||||
exports.convertFile = async (srcFile: string, destFile: string, type: string) => {
|
||||
const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]);
|
||||
let stdoutBuffer = '';
|
||||
abiword.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||
await new Promise((resolve, reject) => {
|
||||
abiword.on('exit', (code) => {
|
||||
abiword.stdout.on('data', (data: string) => { stdoutBuffer += data.toString(); });
|
||||
abiword.stderr.on('data', (data: string) => { stdoutBuffer += data.toString(); });
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
abiword.on('exit', (code: number) => {
|
||||
if (code !== 0) return reject(new Error(`Abiword died with exit code ${code}`));
|
||||
if (stdoutBuffer !== '') {
|
||||
console.log(stdoutBuffer);
|
||||
|
@ -46,13 +49,13 @@ if (os.type().indexOf('Windows') > -1) {
|
|||
// communicate with it via stdin/stdout
|
||||
// thats much faster, about factor 10
|
||||
} else {
|
||||
let abiword;
|
||||
let stdoutCallback = null;
|
||||
let abiword: ChildProcess;
|
||||
let stdoutCallback: Function|null = null;
|
||||
const spawnAbiword = () => {
|
||||
abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']);
|
||||
let stdoutBuffer = '';
|
||||
let firstPrompt = true;
|
||||
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||
abiword.stderr!.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||
abiword.on('exit', (code) => {
|
||||
spawnAbiword();
|
||||
if (stdoutCallback != null) {
|
||||
|
@ -60,7 +63,7 @@ if (os.type().indexOf('Windows') > -1) {
|
|||
stdoutCallback = null;
|
||||
}
|
||||
});
|
||||
abiword.stdout.on('data', (data) => {
|
||||
abiword.stdout!.on('data', (data) => {
|
||||
stdoutBuffer += data.toString();
|
||||
// we're searching for the prompt, cause this means everything we need is in the buffer
|
||||
if (stdoutBuffer.search('AbiWord:>') !== -1) {
|
||||
|
@ -76,15 +79,15 @@ if (os.type().indexOf('Windows') > -1) {
|
|||
};
|
||||
spawnAbiword();
|
||||
|
||||
const queue = async.queue((task, callback) => {
|
||||
abiword.stdin.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`);
|
||||
stdoutCallback = (err) => {
|
||||
const queue = async.queue((task: AsyncQueueTask, callback:Function) => {
|
||||
abiword.stdin!.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`);
|
||||
stdoutCallback = (err: string) => {
|
||||
if (err != null) console.error('Abiword File failed to convert', err);
|
||||
callback(err);
|
||||
};
|
||||
}, 1);
|
||||
|
||||
exports.convertFile = async (srcFile, destFile, type) => {
|
||||
exports.convertFile = async (srcFile: string, destFile: string, type: string) => {
|
||||
await queue.pushAsync({srcFile, destFile, type});
|
||||
};
|
||||
}
|
|
@ -18,7 +18,6 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const log4js = require('log4js');
|
||||
const path = require('path');
|
||||
const _ = require('underscore');
|
||||
|
@ -29,7 +28,7 @@ const absPathLogger = log4js.getLogger('AbsolutePaths');
|
|||
* findEtherpadRoot() computes its value only on first invocation.
|
||||
* Subsequent invocations are served from this variable.
|
||||
*/
|
||||
let etherpadRoot = null;
|
||||
let etherpadRoot: string|null = null;
|
||||
|
||||
/**
|
||||
* If stringArray's last elements are exactly equal to lastDesiredElements,
|
||||
|
@ -41,7 +40,7 @@ let etherpadRoot = null;
|
|||
* @return {string[]|boolean} The shortened array, or false if there was no
|
||||
* overlap.
|
||||
*/
|
||||
const popIfEndsWith = (stringArray, lastDesiredElements) => {
|
||||
const popIfEndsWith = (stringArray: string[], lastDesiredElements: string[]): string[] | false => {
|
||||
if (stringArray.length <= lastDesiredElements.length) {
|
||||
absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" ` +
|
||||
`from "${stringArray.join(path.sep)}", it should contain at least ` +
|
||||
|
@ -131,7 +130,7 @@ exports.findEtherpadRoot = () => {
|
|||
* it is returned unchanged. Otherwise it is interpreted
|
||||
* relative to exports.root.
|
||||
*/
|
||||
exports.makeAbsolute = (somePath) => {
|
||||
exports.makeAbsolute = (somePath: string) => {
|
||||
if (path.isAbsolute(somePath)) {
|
||||
return somePath;
|
||||
}
|
||||
|
@ -150,10 +149,8 @@ exports.makeAbsolute = (somePath) => {
|
|||
* a subdirectory of the base one
|
||||
* @return {boolean}
|
||||
*/
|
||||
exports.isSubdir = (parent, arbitraryDir) => {
|
||||
exports.isSubdir = (parent: string, arbitraryDir: string): boolean => {
|
||||
// modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825
|
||||
const relative = path.relative(parent, arbitraryDir);
|
||||
const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
||||
|
||||
return isSubdir;
|
||||
return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
||||
};
|
|
@ -21,13 +21,13 @@ const authorManager = require('../db/AuthorManager');
|
|||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const padManager = require('../db/PadManager');
|
||||
|
||||
exports.getPadRaw = async (padId, readOnlyId) => {
|
||||
exports.getPadRaw = async (padId:string, readOnlyId:string) => {
|
||||
const dstPfx = `pad:${readOnlyId || padId}`;
|
||||
const [pad, customPrefixes] = await Promise.all([
|
||||
padManager.getPad(padId),
|
||||
hooks.aCallAll('exportEtherpadAdditionalContent'),
|
||||
]);
|
||||
const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix) => {
|
||||
const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix:string) => {
|
||||
const srcPfx = `${customPrefix}:${padId}`;
|
||||
const dstPfx = `${customPrefix}:${readOnlyId || padId}`;
|
||||
assert(!srcPfx.includes('*'));
|
|
@ -26,7 +26,7 @@ const { checkValidRev } = require('./checkValidRev');
|
|||
/*
|
||||
* This method seems unused in core and no plugins depend on it
|
||||
*/
|
||||
exports.getPadPlainText = (pad, revNum) => {
|
||||
exports.getPadPlainText = (pad: { getInternalRevisionAText: (arg0: any) => any; atext: any; pool: any; }, revNum: undefined) => {
|
||||
const _analyzeLine = exports._analyzeLine;
|
||||
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext);
|
||||
const textLines = atext.text.slice(0, -1).split('\n');
|
||||
|
@ -47,10 +47,12 @@ exports.getPadPlainText = (pad, revNum) => {
|
|||
|
||||
return pieces.join('');
|
||||
};
|
||||
type LineModel = {
|
||||
[id:string]:string|number|LineModel
|
||||
}
|
||||
|
||||
|
||||
exports._analyzeLine = (text, aline, apool) => {
|
||||
const line = {};
|
||||
exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => {
|
||||
const line: LineModel = {};
|
||||
|
||||
// identify list
|
||||
let lineMarker = 0;
|
||||
|
@ -86,4 +88,4 @@ exports._analyzeLine = (text, aline, apool) => {
|
|||
|
||||
|
||||
exports._encodeWhitespace =
|
||||
(s) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`);
|
||||
(s:string) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`);
|
|
@ -19,13 +19,16 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {AText, PadType} from "../types/PadType";
|
||||
import {MapType} from "../types/MapType";
|
||||
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const attributes = require('../../static/js/attributes');
|
||||
const padManager = require('../db/PadManager');
|
||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
||||
|
||||
// This is slightly different than the HTML method as it passes the output to getTXTFromAText
|
||||
const getPadTXT = async (pad, revNum) => {
|
||||
const getPadTXT = async (pad: PadType, revNum: string) => {
|
||||
let atext = pad.atext;
|
||||
|
||||
if (revNum !== undefined) {
|
||||
|
@ -39,13 +42,13 @@ const getPadTXT = async (pad, revNum) => {
|
|||
|
||||
// This is different than the functionality provided in ExportHtml as it provides formatting
|
||||
// functionality that is designed specifically for TXT exports
|
||||
const getTXTFromAtext = (pad, atext, authorColors) => {
|
||||
const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||
const apool = pad.apool();
|
||||
const textLines = atext.text.slice(0, -1).split('\n');
|
||||
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
||||
|
||||
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
||||
const anumMap = {};
|
||||
const anumMap: MapType = {};
|
||||
const css = '';
|
||||
|
||||
props.forEach((propName, i) => {
|
||||
|
@ -55,8 +58,8 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
|
|||
}
|
||||
});
|
||||
|
||||
const getLineTXT = (text, attribs) => {
|
||||
const propVals = [false, false, false];
|
||||
const getLineTXT = (text:string, attribs:any) => {
|
||||
const propVals:(number|boolean)[] = [false, false, false];
|
||||
const ENTER = 1;
|
||||
const STAY = 2;
|
||||
const LEAVE = 0;
|
||||
|
@ -71,7 +74,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
|
|||
|
||||
let idx = 0;
|
||||
|
||||
const processNextChars = (numChars) => {
|
||||
const processNextChars = (numChars: number) => {
|
||||
if (numChars <= 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -84,7 +87,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
|
|||
|
||||
for (const a of attributes.decodeAttribString(o.attribs)) {
|
||||
if (a in anumMap) {
|
||||
const i = anumMap[a]; // i = 0 => bold, etc.
|
||||
const i = anumMap[a] as number; // i = 0 => bold, etc.
|
||||
|
||||
if (!propVals[i]) {
|
||||
propVals[i] = ENTER;
|
||||
|
@ -189,7 +192,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
|
|||
// want to deal gracefully with blank lines.
|
||||
// => keeps track of the parents level of indentation
|
||||
|
||||
const listNumbers = {};
|
||||
const listNumbers:MapType = {};
|
||||
let prevListLevel;
|
||||
|
||||
for (let i = 0; i < textLines.length; i++) {
|
||||
|
@ -233,6 +236,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
|
|||
delete listNumbers[prevListLevel];
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
listNumbers[line.listLevel]++;
|
||||
if (line.listLevel > 1) {
|
||||
let x = 1;
|
||||
|
@ -258,7 +262,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
|
|||
|
||||
exports.getTXTFromAtext = getTXTFromAtext;
|
||||
|
||||
exports.getPadTXTDocument = async (padId, revNum) => {
|
||||
exports.getPadTXTDocument = async (padId:string, revNum:string) => {
|
||||
const pad = await padManager.getPad(padId);
|
||||
return getPadTXT(pad, revNum);
|
||||
};
|
|
@ -5,17 +5,19 @@
|
|||
* objects lack.
|
||||
*/
|
||||
class Stream {
|
||||
private _iter
|
||||
private _next: any
|
||||
/**
|
||||
* @returns {Stream} A Stream that yields values in the half-open range [start, end).
|
||||
*/
|
||||
static range(start, end) {
|
||||
static range(start: number, end: number) {
|
||||
return new Stream((function* () { for (let i = start; i < end; ++i) yield i; })());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Iterable<any>} values - Any iterable of values.
|
||||
*/
|
||||
constructor(values) {
|
||||
constructor(values: Iterable<any>) {
|
||||
this._iter = values[Symbol.iterator]();
|
||||
this._next = null;
|
||||
}
|
||||
|
@ -52,10 +54,11 @@ class Stream {
|
|||
* @param {number} size - The number of values to read at a time.
|
||||
* @returns {Stream} A new Stream that gets its values from this Stream.
|
||||
*/
|
||||
batch(size) {
|
||||
batch(size: number) {
|
||||
return new Stream((function* () {
|
||||
const b = [];
|
||||
try {
|
||||
// @ts-ignore
|
||||
for (const v of this) {
|
||||
Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors.
|
||||
b.push(v);
|
||||
|
@ -100,10 +103,11 @@ class Stream {
|
|||
* @param {number} capacity - The number of values to keep buffered.
|
||||
* @returns {Stream} A new Stream that gets its values from this Stream.
|
||||
*/
|
||||
buffer(capacity) {
|
||||
buffer(capacity: number) {
|
||||
return new Stream((function* () {
|
||||
const b = [];
|
||||
try {
|
||||
// @ts-ignore
|
||||
for (const v of this) {
|
||||
Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors.
|
||||
// Note: V8 has good Array push+shift optimization.
|
||||
|
@ -123,7 +127,8 @@ class Stream {
|
|||
* @param {(v: any) => any} fn - Value transformation function.
|
||||
* @returns {Stream} A new Stream that yields this Stream's values, transformed by `fn`.
|
||||
*/
|
||||
map(fn) { return new Stream((function* () { for (const v of this) yield fn(v); }).call(this)); }
|
||||
map(fn:Function) { return new Stream((function* () { // @ts-ignore
|
||||
for (const v of this) yield fn(v); }).call(this)); }
|
||||
|
||||
/**
|
||||
* Implements the JavaScript iterable protocol.
|
|
@ -6,9 +6,14 @@ const headers = {
|
|||
'User-Agent': 'Etherpad/' + settings.getEpVersion(),
|
||||
}
|
||||
|
||||
type Infos = {
|
||||
latestVersion: string
|
||||
}
|
||||
|
||||
|
||||
const updateInterval = 60 * 60 * 1000; // 1 hour
|
||||
let infos;
|
||||
let lastLoadingTime = null;
|
||||
let infos: Infos;
|
||||
let lastLoadingTime: number | null = null;
|
||||
|
||||
const loadEtherpadInformations = () => {
|
||||
if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) {
|
||||
|
@ -16,7 +21,7 @@ const loadEtherpadInformations = () => {
|
|||
}
|
||||
|
||||
return axios.get('https://static.etherpad.org/info.json', {headers: headers})
|
||||
.then(async resp => {
|
||||
.then(async (resp: any) => {
|
||||
infos = await resp.data;
|
||||
if (infos === undefined || infos === null) {
|
||||
await Promise.reject("Could not retrieve current version")
|
||||
|
@ -26,7 +31,7 @@ const loadEtherpadInformations = () => {
|
|||
lastLoadingTime = Date.now();
|
||||
return await Promise.resolve(infos);
|
||||
})
|
||||
.catch(async err => {
|
||||
.catch(async (err: Error) => {
|
||||
return await Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
@ -37,20 +42,20 @@ exports.getLatestVersion = () => {
|
|||
return infos?.latestVersion;
|
||||
};
|
||||
|
||||
exports.needsUpdate = async (cb) => {
|
||||
exports.needsUpdate = async (cb: Function) => {
|
||||
await loadEtherpadInformations()
|
||||
.then((info) => {
|
||||
.then((info:Infos) => {
|
||||
if (semver.gt(info.latestVersion, settings.getEpVersion())) {
|
||||
if (cb) return cb(true);
|
||||
}
|
||||
}).catch((err) => {
|
||||
}).catch((err: Error) => {
|
||||
console.error(`Can not perform Etherpad update check: ${err}`);
|
||||
if (cb) return cb(false);
|
||||
});
|
||||
};
|
||||
|
||||
exports.check = () => {
|
||||
exports.needsUpdate((needsUpdate) => {
|
||||
exports.needsUpdate((needsUpdate: boolean) => {
|
||||
if (needsUpdate) {
|
||||
console.warn(`Update available: Download the actual version ${infos.latestVersion}`);
|
||||
}
|
|
@ -36,32 +36,38 @@ const util = require('util');
|
|||
*
|
||||
*/
|
||||
|
||||
// MIMIC https://github.com/microsoft/TypeScript/commit/9677b0641cc5ba7d8b701b4f892ed7e54ceaee9a - START
|
||||
let _crypto;
|
||||
|
||||
try {
|
||||
_crypto = require('crypto');
|
||||
} catch {
|
||||
_crypto = undefined;
|
||||
}
|
||||
const _crypto = require('crypto');
|
||||
|
||||
|
||||
let CACHE_DIR = path.join(settings.root, 'var/');
|
||||
CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
|
||||
|
||||
const responseCache = {};
|
||||
type Headers = {
|
||||
[id: string]: string
|
||||
}
|
||||
|
||||
const djb2Hash = (data) => {
|
||||
type ResponseCache = {
|
||||
[id: string]: {
|
||||
statusCode: number
|
||||
headers: Headers
|
||||
}
|
||||
}
|
||||
|
||||
const responseCache: ResponseCache = {};
|
||||
|
||||
const djb2Hash = (data: string) => {
|
||||
const chars = data.split('').map((str) => str.charCodeAt(0));
|
||||
return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`;
|
||||
};
|
||||
|
||||
const generateCacheKeyWithSha256 =
|
||||
(path) => _crypto.createHash('sha256').update(path).digest('hex');
|
||||
(path: string) => _crypto.createHash('sha256').update(path).digest('hex');
|
||||
|
||||
const generateCacheKeyWithDjb2 =
|
||||
(path) => Buffer.from(djb2Hash(path)).toString('hex');
|
||||
(path: string) => Buffer.from(djb2Hash(path)).toString('hex');
|
||||
|
||||
let generateCacheKey;
|
||||
let generateCacheKey: (path: string)=>string;
|
||||
|
||||
if (_crypto) {
|
||||
generateCacheKey = generateCacheKeyWithSha256;
|
||||
|
@ -79,17 +85,17 @@ if (_crypto) {
|
|||
*/
|
||||
|
||||
module.exports = class CachingMiddleware {
|
||||
handle(req, res, next) {
|
||||
handle(req: any, res: any, next: any) {
|
||||
this._handle(req, res, next).catch((err) => next(err || new Error(err)));
|
||||
}
|
||||
|
||||
async _handle(req, res, next) {
|
||||
async _handle(req: any, res: any, next: any) {
|
||||
if (!(req.method === 'GET' || req.method === 'HEAD') || !CACHE_DIR) {
|
||||
return next(undefined, req, res);
|
||||
}
|
||||
|
||||
const oldReq = {};
|
||||
const oldRes = {};
|
||||
const oldReq:ResponseCache = {};
|
||||
const oldRes:ResponseCache = {};
|
||||
|
||||
const supportsGzip =
|
||||
(req.get('Accept-Encoding') || '').indexOf('gzip') !== -1;
|
||||
|
@ -119,7 +125,7 @@ module.exports = class CachingMiddleware {
|
|||
res.write = oldRes.write || res.write;
|
||||
res.end = oldRes.end || res.end;
|
||||
|
||||
const headers = {};
|
||||
const headers: Headers = {};
|
||||
Object.assign(headers, (responseCache[cacheKey].headers || {}));
|
||||
const statusCode = responseCache[cacheKey].statusCode;
|
||||
|
||||
|
@ -150,18 +156,19 @@ module.exports = class CachingMiddleware {
|
|||
return respond();
|
||||
}
|
||||
|
||||
const _headers = {};
|
||||
const _headers:Headers = {};
|
||||
oldRes.setHeader = res.setHeader;
|
||||
res.setHeader = (key, value) => {
|
||||
res.setHeader = (key: string, value: string) => {
|
||||
// Don't set cookies, see issue #707
|
||||
if (key.toLowerCase() === 'set-cookie') return;
|
||||
|
||||
_headers[key.toLowerCase()] = value;
|
||||
// @ts-ignore
|
||||
oldRes.setHeader.call(res, key, value);
|
||||
};
|
||||
|
||||
oldRes.writeHead = res.writeHead;
|
||||
res.writeHead = (status, headers) => {
|
||||
res.writeHead = (status: number, headers: Headers) => {
|
||||
res.writeHead = oldRes.writeHead;
|
||||
if (status === 200) {
|
||||
// Update cache
|
||||
|
@ -174,14 +181,14 @@ module.exports = class CachingMiddleware {
|
|||
|
||||
oldRes.write = res.write;
|
||||
oldRes.end = res.end;
|
||||
res.write = (data, encoding) => {
|
||||
res.write = (data: number, encoding: number) => {
|
||||
buffer += data.toString(encoding);
|
||||
};
|
||||
res.end = async (data, encoding) => {
|
||||
res.end = async (data: number, encoding: number) => {
|
||||
await Promise.all([
|
||||
fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}`, buffer).catch(() => {}),
|
||||
util.promisify(zlib.gzip)(buffer)
|
||||
.then((content) => fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}.gz`, content))
|
||||
.then((content: string) => fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}.gz`, content))
|
||||
.catch(() => {}),
|
||||
]);
|
||||
responseCache[cacheKey] = {statusCode: status, headers};
|
||||
|
@ -191,8 +198,8 @@ module.exports = class CachingMiddleware {
|
|||
// Nothing new changed from the cached version.
|
||||
oldRes.write = res.write;
|
||||
oldRes.end = res.end;
|
||||
res.write = (data, encoding) => {};
|
||||
res.end = (data, encoding) => { respond(); };
|
||||
res.write = (data: number, encoding: number) => {};
|
||||
res.end = (data: number, encoding: number) => { respond(); };
|
||||
} else {
|
||||
res.writeHead(status, headers);
|
||||
}
|
|
@ -4,7 +4,7 @@ const CustomError = require('../utils/customError');
|
|||
|
||||
// checks if a rev is a legal number
|
||||
// pre-condition is that `rev` is not undefined
|
||||
const checkValidRev = (rev) => {
|
||||
const checkValidRev = (rev: number|string) => {
|
||||
if (typeof rev !== 'number') {
|
||||
rev = parseInt(rev, 10);
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ const checkValidRev = (rev) => {
|
|||
};
|
||||
|
||||
// checks if a number is an int
|
||||
const isInt = (value) => (parseFloat(value) === parseInt(value, 10)) && !isNaN(value);
|
||||
const isInt = (value:number) => (parseFloat(String(value)) === parseInt(String(value), 10)) && !isNaN(value);
|
||||
|
||||
exports.isInt = isInt;
|
||||
exports.checkValidRev = checkValidRev;
|
|
@ -10,11 +10,11 @@
|
|||
class CustomError extends Error {
|
||||
/**
|
||||
* Creates an instance of CustomError.
|
||||
* @param {*} message
|
||||
* @param {string} message
|
||||
* @param {string} [name='Error'] a custom name for the error object
|
||||
* @memberof CustomError
|
||||
*/
|
||||
constructor(message, name = 'Error') {
|
||||
constructor(message:string, name: string = 'Error') {
|
||||
super(message);
|
||||
this.name = name;
|
||||
Error.captureStackTrace(this, this.constructor);
|
|
@ -1,8 +1,8 @@
|
|||
'use strict';
|
||||
const fs = require('fs');
|
||||
|
||||
const check = (path) => {
|
||||
const existsSync = fs.statSync || fs.existsSync || path.existsSync;
|
||||
const check = (path:string) => {
|
||||
const existsSync = fs.statSync || fs.existsSync;
|
||||
|
||||
let result;
|
||||
try {
|
|
@ -7,14 +7,16 @@
|
|||
// `predicate`. Resolves to `undefined` if none of the Promises satisfy `predicate`, or if
|
||||
// `promises` is empty. If `predicate` is nullish, the truthiness of the resolved value is used as
|
||||
// the predicate.
|
||||
exports.firstSatisfies = (promises, predicate) => {
|
||||
if (predicate == null) predicate = (x) => x;
|
||||
exports.firstSatisfies = <T>(promises: Promise<T>[], predicate: null|Function) => {
|
||||
if (predicate == null) {
|
||||
predicate = (x: any) => x;
|
||||
}
|
||||
|
||||
// Transform each original Promise into a Promise that never resolves if the original resolved
|
||||
// value does not satisfy `predicate`. These transformed Promises will be passed to Promise.race,
|
||||
// yielding the first resolved value that satisfies `predicate`.
|
||||
const newPromises = promises.map(
|
||||
(p) => new Promise((resolve, reject) => p.then((v) => predicate(v) && resolve(v), reject)));
|
||||
const newPromises = promises.map((p) =>
|
||||
new Promise((resolve, reject) => p.then((v) => predicate!(v) && resolve(v), reject)));
|
||||
|
||||
// If `promises` is an empty array or if none of them resolve to a value that satisfies
|
||||
// `predicate`, then `Promise.race(newPromises)` will never resolve. To handle that, add another
|
||||
|
@ -42,7 +44,7 @@ exports.firstSatisfies = (promises, predicate) => {
|
|||
// `total` is greater than `concurrency`, then `concurrency` Promises will be created right away,
|
||||
// and each remaining Promise will be created once one of the earlier Promises resolves.) This async
|
||||
// function resolves once all `total` Promises have resolved.
|
||||
exports.timesLimit = async (total, concurrency, promiseCreator) => {
|
||||
exports.timesLimit = async (total: number, concurrency: number, promiseCreator: Function) => {
|
||||
if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive');
|
||||
let next = 0;
|
||||
const addAnother = () => promiseCreator(next++).finally(() => {
|
||||
|
@ -59,7 +61,7 @@ exports.timesLimit = async (total, concurrency, promiseCreator) => {
|
|||
* An ordinary Promise except the `resolve` and `reject` executor functions are exposed as
|
||||
* properties.
|
||||
*/
|
||||
class Gate extends Promise {
|
||||
class Gate<T> extends Promise<T> {
|
||||
// Coax `.then()` into returning an ordinary Promise, not a Gate. See
|
||||
// https://stackoverflow.com/a/65669070 for the rationale.
|
||||
static get [Symbol.species]() { return Promise; }
|
||||
|
@ -68,7 +70,7 @@ class Gate extends Promise {
|
|||
// `this` is assigned when `super()` returns, not when it is called, so it is not acceptable to
|
||||
// do the following because it will throw a ReferenceError when it dereferences `this`:
|
||||
// super((resolve, reject) => Object.assign(this, {resolve, reject}));
|
||||
let props;
|
||||
let props: any;
|
||||
super((resolve, reject) => props = {resolve, reject});
|
||||
Object.assign(this, props);
|
||||
}
|
|
@ -3,8 +3,8 @@
|
|||
* Generates a random String with the given length. Is needed to generate the
|
||||
* Author, Group, readonly, session Ids
|
||||
*/
|
||||
const crypto = require('crypto');
|
||||
const cryptoMod = require('crypto');
|
||||
|
||||
const randomString = (len) => crypto.randomBytes(len).toString('hex');
|
||||
const randomString = (len: number) => cryptoMod.randomBytes(len).toString('hex');
|
||||
|
||||
module.exports = randomString;
|
|
@ -1,5 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
import {ErrorExtended, RunCMDOptions, RunCMDPromise} from "../types/RunCMDOptions";
|
||||
import {ChildProcess} from "node:child_process";
|
||||
import {PromiseWithStd} from "../types/PromiseWithStd";
|
||||
import {Readable} from "node:stream";
|
||||
|
||||
const spawn = require('cross-spawn');
|
||||
const log4js = require('log4js');
|
||||
const path = require('path');
|
||||
|
@ -7,12 +12,12 @@ const settings = require('./Settings');
|
|||
|
||||
const logger = log4js.getLogger('runCmd');
|
||||
|
||||
const logLines = (readable, logLineFn) => {
|
||||
readable.setEncoding('utf8');
|
||||
const logLines = (readable: undefined | Readable | null, logLineFn: (arg0: (string | undefined)) => void) => {
|
||||
readable!.setEncoding('utf8');
|
||||
// The process won't necessarily write full lines every time -- it might write a part of a line
|
||||
// then write the rest of the line later.
|
||||
let leftovers = '';
|
||||
readable.on('data', (chunk) => {
|
||||
let leftovers: string| undefined = '';
|
||||
readable!.on('data', (chunk) => {
|
||||
const lines = chunk.split('\n');
|
||||
if (lines.length === 0) return;
|
||||
lines[0] = leftovers + lines[0];
|
||||
|
@ -21,7 +26,7 @@ const logLines = (readable, logLineFn) => {
|
|||
logLineFn(line);
|
||||
}
|
||||
});
|
||||
readable.on('end', () => {
|
||||
readable!.on('end', () => {
|
||||
if (leftovers !== '') logLineFn(leftovers);
|
||||
leftovers = '';
|
||||
});
|
||||
|
@ -69,7 +74,7 @@ const logLines = (readable, logLineFn) => {
|
|||
* - `stderr`: Similar to `stdout` but for stderr.
|
||||
* - `child`: The ChildProcess object.
|
||||
*/
|
||||
module.exports = exports = (args, opts = {}) => {
|
||||
module.exports = exports = (args: string[], opts:RunCMDOptions = {}) => {
|
||||
logger.debug(`Executing command: ${args.join(' ')}`);
|
||||
|
||||
opts = {cwd: settings.root, ...opts};
|
||||
|
@ -82,8 +87,8 @@ module.exports = exports = (args, opts = {}) => {
|
|||
: opts.stdio === 'string' ? [null, 'string', 'string']
|
||||
: Array(3).fill(opts.stdio);
|
||||
const cmdLogger = log4js.getLogger(`runCmd|${args[0]}`);
|
||||
if (stdio[1] == null) stdio[1] = (line) => cmdLogger.info(line);
|
||||
if (stdio[2] == null) stdio[2] = (line) => cmdLogger.error(line);
|
||||
if (stdio[1] == null) stdio[1] = (line: string) => cmdLogger.info(line);
|
||||
if (stdio[2] == null) stdio[2] = (line: string) => cmdLogger.error(line);
|
||||
const stdioLoggers = [];
|
||||
const stdioSaveString = [];
|
||||
for (const fd of [1, 2]) {
|
||||
|
@ -116,13 +121,13 @@ module.exports = exports = (args, opts = {}) => {
|
|||
|
||||
// Create an error object to use in case the process fails. This is done here rather than in the
|
||||
// process's `exit` handler so that we get a useful stack trace.
|
||||
const procFailedErr = new Error();
|
||||
const procFailedErr: Error & ErrorExtended = new Error();
|
||||
|
||||
const proc = spawn(args[0], args.slice(1), opts);
|
||||
const streams = [undefined, proc.stdout, proc.stderr];
|
||||
const proc: ChildProcess = spawn(args[0], args.slice(1), opts);
|
||||
const streams:[undefined, Readable|null, Readable|null] = [undefined, proc.stdout, proc.stderr];
|
||||
|
||||
let px;
|
||||
const p = new Promise((resolve, reject) => { px = {resolve, reject}; });
|
||||
let px: { reject: any; resolve: any; };
|
||||
const p:PromiseWithStd = new Promise<string>((resolve, reject) => { px = {resolve, reject}; });
|
||||
[, p.stdout, p.stderr] = streams;
|
||||
p.child = proc;
|
||||
|
||||
|
@ -132,9 +137,10 @@ module.exports = exports = (args, opts = {}) => {
|
|||
if (stdioLoggers[fd] != null) {
|
||||
logLines(streams[fd], stdioLoggers[fd]);
|
||||
} else if (stdioSaveString[fd]) {
|
||||
// @ts-ignore
|
||||
p[[null, 'stdout', 'stderr'][fd]] = stdioStringPromises[fd] = (async () => {
|
||||
const chunks = [];
|
||||
for await (const chunk of streams[fd]) chunks.push(chunk);
|
||||
for await (const chunk of streams[fd]!) chunks.push(chunk);
|
||||
return Buffer.concat(chunks).toString().replace(/\n+$/g, '');
|
||||
})();
|
||||
}
|
|
@ -4,7 +4,7 @@ const path = require('path');
|
|||
|
||||
// Normalizes p and ensures that it is a relative path that does not reach outside. See
|
||||
// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context.
|
||||
module.exports = (p, pathApi = path) => {
|
||||
module.exports = (p: string, pathApi = path) => {
|
||||
// The documentation for path.normalize() says that it resolves '..' and '.' segments. The word
|
||||
// "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might
|
||||
// not be the same thing as 'b'. Most path normalization functions from other libraries (e.g.,
|
|
@ -1,270 +0,0 @@
|
|||
'use strict';
|
||||
/**
|
||||
* The Toolbar Module creates and renders the toolbars and buttons
|
||||
*/
|
||||
const _ = require('underscore');
|
||||
|
||||
const removeItem = (array, what) => {
|
||||
let ax;
|
||||
while ((ax = array.indexOf(what)) !== -1) {
|
||||
array.splice(ax, 1);
|
||||
}
|
||||
return array;
|
||||
};
|
||||
|
||||
const defaultButtonAttributes = (name, overrides) => ({
|
||||
command: name,
|
||||
localizationId: `pad.toolbar.${name}.title`,
|
||||
class: `buttonicon buttonicon-${name}`,
|
||||
});
|
||||
|
||||
const tag = (name, attributes, contents) => {
|
||||
const aStr = tagAttributes(attributes);
|
||||
|
||||
if (_.isString(contents) && contents.length > 0) {
|
||||
return `<${name}${aStr}>${contents}</${name}>`;
|
||||
} else {
|
||||
return `<${name}${aStr}></${name}>`;
|
||||
}
|
||||
};
|
||||
|
||||
const tagAttributes = (attributes) => {
|
||||
attributes = _.reduce(attributes || {}, (o, val, name) => {
|
||||
if (!_.isUndefined(val)) {
|
||||
o[name] = val;
|
||||
}
|
||||
return o;
|
||||
}, {});
|
||||
|
||||
return ` ${_.map(attributes, (val, name) => `${name}="${_.escape(val)}"`).join(' ')}`;
|
||||
};
|
||||
|
||||
const ButtonsGroup = function () {
|
||||
this.buttons = [];
|
||||
};
|
||||
|
||||
ButtonsGroup.fromArray = function (array) {
|
||||
const btnGroup = new this();
|
||||
_.each(array, (btnName) => {
|
||||
btnGroup.addButton(Button.load(btnName));
|
||||
});
|
||||
return btnGroup;
|
||||
};
|
||||
|
||||
ButtonsGroup.prototype.addButton = function (button) {
|
||||
this.buttons.push(button);
|
||||
return this;
|
||||
};
|
||||
|
||||
ButtonsGroup.prototype.render = function () {
|
||||
if (this.buttons && this.buttons.length === 1) {
|
||||
this.buttons[0].grouping = '';
|
||||
} else if (this.buttons && this.buttons.length > 1) {
|
||||
_.first(this.buttons).grouping = 'grouped-left';
|
||||
_.last(this.buttons).grouping = 'grouped-right';
|
||||
_.each(this.buttons.slice(1, -1), (btn) => {
|
||||
btn.grouping = 'grouped-middle';
|
||||
});
|
||||
}
|
||||
|
||||
return _.map(this.buttons, (btn) => {
|
||||
if (btn) return btn.render();
|
||||
}).join('\n');
|
||||
};
|
||||
|
||||
const Button = function (attributes) {
|
||||
this.attributes = attributes;
|
||||
};
|
||||
|
||||
Button.load = (btnName) => {
|
||||
const button = module.exports.availableButtons[btnName];
|
||||
try {
|
||||
if (button.constructor === Button || button.constructor === SelectButton) {
|
||||
return button;
|
||||
} else {
|
||||
return new Button(button);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error loading button', btnName);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
_.extend(Button.prototype, {
|
||||
grouping: '',
|
||||
|
||||
render() {
|
||||
const liAttributes = {
|
||||
'data-type': 'button',
|
||||
'data-key': this.attributes.command,
|
||||
};
|
||||
return tag('li', liAttributes,
|
||||
tag('a', {'class': this.grouping, 'data-l10n-id': this.attributes.localizationId},
|
||||
tag('button', {
|
||||
'class': ` ${this.attributes.class}`,
|
||||
'data-l10n-id': this.attributes.localizationId,
|
||||
})));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const SelectButton = function (attributes) {
|
||||
this.attributes = attributes;
|
||||
this.options = [];
|
||||
};
|
||||
|
||||
_.extend(SelectButton.prototype, Button.prototype, {
|
||||
addOption(value, text, attributes) {
|
||||
this.options.push({
|
||||
value,
|
||||
text,
|
||||
attributes,
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
select(attributes) {
|
||||
const options = [];
|
||||
|
||||
_.each(this.options, (opt) => {
|
||||
const a = _.extend({
|
||||
value: opt.value,
|
||||
}, opt.attributes);
|
||||
|
||||
options.push(tag('option', a, opt.text));
|
||||
});
|
||||
return tag('select', attributes, options.join(''));
|
||||
},
|
||||
|
||||
render() {
|
||||
const attributes = {
|
||||
'id': this.attributes.id,
|
||||
'data-key': this.attributes.command,
|
||||
'data-type': 'select',
|
||||
};
|
||||
return tag('li', attributes, this.select({id: this.attributes.selectId}));
|
||||
},
|
||||
});
|
||||
|
||||
const Separator = function () {};
|
||||
Separator.prototype.render = function () {
|
||||
return tag('li', {class: 'separator'});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
availableButtons: {
|
||||
bold: defaultButtonAttributes('bold'),
|
||||
italic: defaultButtonAttributes('italic'),
|
||||
underline: defaultButtonAttributes('underline'),
|
||||
strikethrough: defaultButtonAttributes('strikethrough'),
|
||||
|
||||
orderedlist: {
|
||||
command: 'insertorderedlist',
|
||||
localizationId: 'pad.toolbar.ol.title',
|
||||
class: 'buttonicon buttonicon-insertorderedlist',
|
||||
},
|
||||
|
||||
unorderedlist: {
|
||||
command: 'insertunorderedlist',
|
||||
localizationId: 'pad.toolbar.ul.title',
|
||||
class: 'buttonicon buttonicon-insertunorderedlist',
|
||||
},
|
||||
|
||||
indent: defaultButtonAttributes('indent'),
|
||||
outdent: {
|
||||
command: 'outdent',
|
||||
localizationId: 'pad.toolbar.unindent.title',
|
||||
class: 'buttonicon buttonicon-outdent',
|
||||
},
|
||||
|
||||
undo: defaultButtonAttributes('undo'),
|
||||
redo: defaultButtonAttributes('redo'),
|
||||
|
||||
clearauthorship: {
|
||||
command: 'clearauthorship',
|
||||
localizationId: 'pad.toolbar.clearAuthorship.title',
|
||||
class: 'buttonicon buttonicon-clearauthorship',
|
||||
},
|
||||
|
||||
importexport: {
|
||||
command: 'import_export',
|
||||
localizationId: 'pad.toolbar.import_export.title',
|
||||
class: 'buttonicon buttonicon-import_export',
|
||||
},
|
||||
|
||||
timeslider: {
|
||||
command: 'showTimeSlider',
|
||||
localizationId: 'pad.toolbar.timeslider.title',
|
||||
class: 'buttonicon buttonicon-history',
|
||||
},
|
||||
|
||||
savedrevision: defaultButtonAttributes('savedRevision'),
|
||||
settings: defaultButtonAttributes('settings'),
|
||||
embed: defaultButtonAttributes('embed'),
|
||||
showusers: defaultButtonAttributes('showusers'),
|
||||
|
||||
timeslider_export: {
|
||||
command: 'import_export',
|
||||
localizationId: 'timeslider.toolbar.exportlink.title',
|
||||
class: 'buttonicon buttonicon-import_export',
|
||||
},
|
||||
|
||||
timeslider_settings: {
|
||||
command: 'settings',
|
||||
localizationId: 'pad.toolbar.settings.title',
|
||||
class: 'buttonicon buttonicon-settings',
|
||||
},
|
||||
|
||||
timeslider_returnToPad: {
|
||||
command: 'timeslider_returnToPad',
|
||||
localizationId: 'timeslider.toolbar.returnbutton',
|
||||
class: 'buttontext',
|
||||
},
|
||||
},
|
||||
|
||||
registerButton(buttonName, buttonInfo) {
|
||||
this.availableButtons[buttonName] = buttonInfo;
|
||||
},
|
||||
|
||||
button: (attributes) => new Button(attributes),
|
||||
|
||||
separator: () => (new Separator()).render(),
|
||||
|
||||
selectButton: (attributes) => new SelectButton(attributes),
|
||||
|
||||
/*
|
||||
* Valid values for whichMenu: 'left' | 'right' | 'timeslider-right'
|
||||
* Valid values for page: 'pad' | 'timeslider'
|
||||
*/
|
||||
menu(buttons, isReadOnly, whichMenu, page) {
|
||||
if (isReadOnly) {
|
||||
// The best way to detect if it's the left editbar is to check for a bold button
|
||||
if (buttons[0].indexOf('bold') !== -1) {
|
||||
// Clear all formatting buttons
|
||||
buttons = [];
|
||||
} else {
|
||||
// Remove Save Revision from the right menu
|
||||
removeItem(buttons[0], 'savedrevision');
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* This pad is not read only
|
||||
*
|
||||
* Add back the savedrevision button (the "star") if is not already there,
|
||||
* but only on the right toolbar, and only if we are showing a pad (dont't
|
||||
* do it in the timeslider).
|
||||
*
|
||||
* This is a quick fix for #3702 (and subsequent issue #3767): it was
|
||||
* sufficient to visit a single read only pad to cause the disappearence
|
||||
* of the star button from all the pads.
|
||||
*/
|
||||
if ((buttons[0].indexOf('savedrevision') === -1) &&
|
||||
(whichMenu === 'right') && (page === 'pad')) {
|
||||
buttons[0].push('savedrevision');
|
||||
}
|
||||
}
|
||||
|
||||
const groups = _.map(buttons, (group) => ButtonsGroup.fromArray(group).render());
|
||||
return groups.join(this.separator());
|
||||
},
|
||||
};
|
305
src/node/utils/toolbar.ts
Normal file
305
src/node/utils/toolbar.ts
Normal file
|
@ -0,0 +1,305 @@
|
|||
'use strict';
|
||||
/**
|
||||
* The Toolbar Module creates and renders the toolbars and buttons
|
||||
*/
|
||||
const _ = require('underscore');
|
||||
|
||||
const removeItem = (array: string[], what: string) => {
|
||||
let ax;
|
||||
while ((ax = array.indexOf(what)) !== -1) {
|
||||
array.splice(ax, 1);
|
||||
}
|
||||
return array;
|
||||
};
|
||||
|
||||
const defaultButtonAttributes = (name: string, overrides?: boolean) => ({
|
||||
command: name,
|
||||
localizationId: `pad.toolbar.${name}.title`,
|
||||
class: `buttonicon buttonicon-${name}`,
|
||||
});
|
||||
|
||||
const tag = (name: string, attributes: AttributeObj, contents?: string) => {
|
||||
const aStr = tagAttributes(attributes);
|
||||
|
||||
if (_.isString(contents) && contents!.length > 0) {
|
||||
return `<${name}${aStr}>${contents}</${name}>`;
|
||||
} else {
|
||||
return `<${name}${aStr}></${name}>`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
type AttributeObj = {
|
||||
[id: string]: string
|
||||
}
|
||||
|
||||
const tagAttributes = (attributes: AttributeObj) => {
|
||||
attributes = _.reduce(attributes || {}, (o: AttributeObj, val: string, name: string) => {
|
||||
if (!_.isUndefined(val)) {
|
||||
o[name] = val;
|
||||
}
|
||||
return o;
|
||||
}, {});
|
||||
|
||||
return ` ${_.map(attributes, (val: string, name: string) => `${name}="${_.escape(val)}"`).join(' ')}`;
|
||||
};
|
||||
|
||||
type ButtonGroupType = {
|
||||
grouping: string,
|
||||
render: Function
|
||||
}
|
||||
|
||||
class ButtonGroup {
|
||||
private buttons: Button[]
|
||||
|
||||
constructor() {
|
||||
this.buttons = []
|
||||
}
|
||||
|
||||
public static fromArray = function (array: string[]) {
|
||||
const btnGroup = new ButtonGroup();
|
||||
_.each(array, (btnName: string) => {
|
||||
const button = Button.load(btnName) as Button
|
||||
btnGroup.addButton(button);
|
||||
});
|
||||
return btnGroup;
|
||||
}
|
||||
|
||||
private addButton(button: Button) {
|
||||
this.buttons.push(button);
|
||||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.buttons && this.buttons.length === 1) {
|
||||
this.buttons[0].grouping = '';
|
||||
} else if (this.buttons && this.buttons.length > 1) {
|
||||
_.first(this.buttons).grouping = 'grouped-left';
|
||||
_.last(this.buttons).grouping = 'grouped-right';
|
||||
_.each(this.buttons.slice(1, -1), (btn: Button) => {
|
||||
btn.grouping = 'grouped-middle';
|
||||
});
|
||||
}
|
||||
|
||||
return _.map(this.buttons, (btn: ButtonGroup) => {
|
||||
if (btn) return btn.render();
|
||||
}).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Button {
|
||||
protected attributes: AttributeObj
|
||||
grouping: string
|
||||
|
||||
constructor(attributes: AttributeObj) {
|
||||
this.attributes = attributes
|
||||
this.grouping = ""
|
||||
}
|
||||
|
||||
public static load(btnName: string) {
|
||||
const button = module.exports.availableButtons[btnName];
|
||||
try {
|
||||
if (button.constructor === Button || button.constructor === SelectButton) {
|
||||
return button;
|
||||
} else {
|
||||
return new Button(button);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error loading button', btnName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const liAttributes = {
|
||||
'data-type': 'button',
|
||||
'data-key': this.attributes.command,
|
||||
};
|
||||
return tag('li', liAttributes,
|
||||
tag('a', {'class': this.grouping, 'data-l10n-id': this.attributes.localizationId},
|
||||
tag('button', {
|
||||
'class': ` ${this.attributes.class}`,
|
||||
'data-l10n-id': this.attributes.localizationId,
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
type SelectButtonOptions = {
|
||||
value: string,
|
||||
text: string,
|
||||
attributes: AttributeObj
|
||||
}
|
||||
|
||||
class SelectButton extends Button {
|
||||
private readonly options: SelectButtonOptions[];
|
||||
|
||||
constructor(attrs: AttributeObj) {
|
||||
super(attrs);
|
||||
this.options = []
|
||||
}
|
||||
|
||||
addOption(value: string, text: string, attributes: AttributeObj) {
|
||||
this.options.push({
|
||||
value,
|
||||
text,
|
||||
attributes,
|
||||
})
|
||||
return this;
|
||||
}
|
||||
|
||||
select(attributes: AttributeObj) {
|
||||
const options: string[] = [];
|
||||
|
||||
_.each(this.options, (opt: AttributeSelect) => {
|
||||
const a = _.extend({
|
||||
value: opt.value,
|
||||
}, opt.attributes);
|
||||
|
||||
options.push(tag('option', a, opt.text));
|
||||
});
|
||||
return tag('select', attributes, options.join(''));
|
||||
}
|
||||
|
||||
render() {
|
||||
const attributes = {
|
||||
'id': this.attributes.id,
|
||||
'data-key': this.attributes.command,
|
||||
'data-type': 'select',
|
||||
};
|
||||
return tag('li', attributes, this.select({id: this.attributes.selectId}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type AttributeSelect = {
|
||||
value: string,
|
||||
attributes: AttributeObj,
|
||||
text: string
|
||||
}
|
||||
|
||||
class Separator {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public render() {
|
||||
return tag('li', {class: 'separator'});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
availableButtons: {
|
||||
bold: defaultButtonAttributes('bold'),
|
||||
italic: defaultButtonAttributes('italic'),
|
||||
underline: defaultButtonAttributes('underline'),
|
||||
strikethrough: defaultButtonAttributes('strikethrough'),
|
||||
|
||||
orderedlist: {
|
||||
command: 'insertorderedlist',
|
||||
localizationId: 'pad.toolbar.ol.title',
|
||||
class: 'buttonicon buttonicon-insertorderedlist',
|
||||
},
|
||||
|
||||
unorderedlist: {
|
||||
command: 'insertunorderedlist',
|
||||
localizationId: 'pad.toolbar.ul.title',
|
||||
class: 'buttonicon buttonicon-insertunorderedlist',
|
||||
},
|
||||
|
||||
indent: defaultButtonAttributes('indent'),
|
||||
outdent: {
|
||||
command: 'outdent',
|
||||
localizationId: 'pad.toolbar.unindent.title',
|
||||
class: 'buttonicon buttonicon-outdent',
|
||||
},
|
||||
|
||||
undo: defaultButtonAttributes('undo'),
|
||||
redo: defaultButtonAttributes('redo'),
|
||||
|
||||
clearauthorship: {
|
||||
command: 'clearauthorship',
|
||||
localizationId: 'pad.toolbar.clearAuthorship.title',
|
||||
class: 'buttonicon buttonicon-clearauthorship',
|
||||
},
|
||||
|
||||
importexport: {
|
||||
command: 'import_export',
|
||||
localizationId: 'pad.toolbar.import_export.title',
|
||||
class: 'buttonicon buttonicon-import_export',
|
||||
},
|
||||
|
||||
timeslider: {
|
||||
command: 'showTimeSlider',
|
||||
localizationId: 'pad.toolbar.timeslider.title',
|
||||
class: 'buttonicon buttonicon-history',
|
||||
},
|
||||
|
||||
savedrevision: defaultButtonAttributes('savedRevision'),
|
||||
settings: defaultButtonAttributes('settings'),
|
||||
embed: defaultButtonAttributes('embed'),
|
||||
showusers: defaultButtonAttributes('showusers'),
|
||||
|
||||
timeslider_export: {
|
||||
command: 'import_export',
|
||||
localizationId: 'timeslider.toolbar.exportlink.title',
|
||||
class: 'buttonicon buttonicon-import_export',
|
||||
},
|
||||
|
||||
timeslider_settings: {
|
||||
command: 'settings',
|
||||
localizationId: 'pad.toolbar.settings.title',
|
||||
class: 'buttonicon buttonicon-settings',
|
||||
},
|
||||
|
||||
timeslider_returnToPad: {
|
||||
command: 'timeslider_returnToPad',
|
||||
localizationId: 'timeslider.toolbar.returnbutton',
|
||||
class: 'buttontext',
|
||||
},
|
||||
},
|
||||
|
||||
registerButton(buttonName: string, buttonInfo: any) {
|
||||
this.availableButtons[buttonName] = buttonInfo;
|
||||
},
|
||||
|
||||
button: (attributes: AttributeObj) => new Button(attributes),
|
||||
|
||||
separator: () => (new Separator()).render(),
|
||||
|
||||
selectButton: (attributes: AttributeObj) => new SelectButton(attributes),
|
||||
|
||||
/*
|
||||
* Valid values for whichMenu: 'left' | 'right' | 'timeslider-right'
|
||||
* Valid values for page: 'pad' | 'timeslider'
|
||||
*/
|
||||
menu(buttons: string[][], isReadOnly: boolean, whichMenu: string, page: string) {
|
||||
if (isReadOnly) {
|
||||
// The best way to detect if it's the left editbar is to check for a bold button
|
||||
if (buttons[0].indexOf('bold') !== -1) {
|
||||
// Clear all formatting buttons
|
||||
buttons = [];
|
||||
} else {
|
||||
// Remove Save Revision from the right menu
|
||||
removeItem(buttons[0], 'savedrevision');
|
||||
}
|
||||
} else if ((buttons[0].indexOf('savedrevision') === -1) &&
|
||||
(whichMenu === 'right') && (page === 'pad')) {
|
||||
/*
|
||||
* This pad is not read only
|
||||
*
|
||||
* Add back the savedrevision button (the "star") if is not already there,
|
||||
* but only on the right toolbar, and only if we are showing a pad (dont't
|
||||
* do it in the timeslider).
|
||||
*
|
||||
* This is a quick fix for #3702 (and subsequent issue #3767): it was
|
||||
* sufficient to visit a single read only pad to cause the disappearence
|
||||
* of the star button from all the pads.
|
||||
*/
|
||||
buttons[0].push('savedrevision');
|
||||
}
|
||||
|
||||
const groups = _.map(buttons, (group: string[]) => ButtonGroup.fromArray(group).render());
|
||||
return groups.join(this.separator());
|
||||
},
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue