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:
SamTV12345 2024-02-05 21:13:02 +01:00 committed by GitHub
parent c3202284bc
commit ead3c0ea38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 1259 additions and 612 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)) {

View file

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

View file

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