diff --git a/src/node/db/DB.js b/src/node/db/DB.ts similarity index 92% rename from src/node/db/DB.js rename to src/node/db/DB.ts index 02e83f85d..bd8b8e9a2 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.ts @@ -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'); diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.ts similarity index 94% rename from src/node/db/SessionStore.js rename to src/node/db/SessionStore.ts index 40e5e90d0..5dca5e201 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.ts @@ -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. diff --git a/src/node/models/AsyncQueueTask.ts b/src/node/models/AsyncQueueTask.ts new file mode 100644 index 000000000..03a915ac7 --- /dev/null +++ b/src/node/models/AsyncQueueTask.ts @@ -0,0 +1,5 @@ +export type AsyncQueueTask = { + srcFile: string, + destFile: string, + type: string +} \ No newline at end of file diff --git a/src/node/models/PromiseWithStd.ts b/src/node/models/PromiseWithStd.ts new file mode 100644 index 000000000..46b03076a --- /dev/null +++ b/src/node/models/PromiseWithStd.ts @@ -0,0 +1,8 @@ +import {Readable} from "node:stream"; +import {ChildProcess} from "node:child_process"; + +export type PromiseWithStd = { + stdout?: Readable|null, + stderr?: Readable|null, + child?: ChildProcess +} & Promise \ No newline at end of file diff --git a/src/node/models/RunCMDOptions.ts b/src/node/models/RunCMDOptions.ts new file mode 100644 index 000000000..74298f221 --- /dev/null +++ b/src/node/models/RunCMDOptions.ts @@ -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 +} \ No newline at end of file diff --git a/src/node/padaccess.js b/src/node/padaccess.ts similarity index 70% rename from src/node/padaccess.js rename to src/node/padaccess.ts index e9cc7cde5..ce3cf9ddd 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.ts @@ -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); diff --git a/src/node/stats.js b/src/node/stats.ts similarity index 65% rename from src/node/stats.js rename to src/node/stats.ts index cecaca20d..2767b542f 100644 --- a/src/node/stats.js +++ b/src/node/stats.ts @@ -4,6 +4,6 @@ const measured = require('measured-core'); module.exports = measured.createCollection(); -module.exports.shutdown = async (hookName, context) => { +module.exports.shutdown = async (hookName: string, context:any) => { module.exports.end(); }; diff --git a/src/node/utils/Abiword.js b/src/node/utils/Abiword.ts similarity index 72% rename from src/node/utils/Abiword.js rename to src/node/utils/Abiword.ts index 1ed487ae1..74034b23b 100644 --- a/src/node/utils/Abiword.js +++ b/src/node/utils/Abiword.ts @@ -19,6 +19,9 @@ * limitations under the License. */ +import {ChildProcess} from "node:child_process"; +import {AsyncQueueTask} from "../models/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((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}); }; } diff --git a/src/node/utils/AbsolutePaths.js b/src/node/utils/AbsolutePaths.ts similarity index 94% rename from src/node/utils/AbsolutePaths.js rename to src/node/utils/AbsolutePaths.ts index 73a96bb67..c257440a1 100644 --- a/src/node/utils/AbsolutePaths.js +++ b/src/node/utils/AbsolutePaths.ts @@ -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); }; diff --git a/src/node/utils/randomstring.js b/src/node/utils/randomstring.ts similarity index 59% rename from src/node/utils/randomstring.js rename to src/node/utils/randomstring.ts index 4ffd3e8ae..f60fae1f3 100644 --- a/src/node/utils/randomstring.js +++ b/src/node/utils/randomstring.ts @@ -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: string) => cryptoMod.randomBytes(len).toString('hex'); module.exports = randomString; diff --git a/src/node/utils/run_cmd.js b/src/node/utils/run_cmd.ts similarity index 83% rename from src/node/utils/run_cmd.js rename to src/node/utils/run_cmd.ts index bf5515c84..083d43c5c 100644 --- a/src/node/utils/run_cmd.js +++ b/src/node/utils/run_cmd.ts @@ -1,5 +1,10 @@ 'use strict'; +import {ErrorExtended, RunCMDOptions, RunCMDPromise} from "../models/RunCMDOptions"; +import {ChildProcess} from "node:child_process"; +import {PromiseWithStd} from "../models/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((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, ''); })(); } diff --git a/src/package-lock.json b/src/package-lock.json index eacad0e18..9f2964398 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -409,6 +409,12 @@ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "dev": true }, + "@types/async": { + "version": "3.2.24", + "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.24.tgz", + "integrity": "sha512-8iHVLHsCCOBKjCF2KwFe0p9Z3rfM9mL+sSP8btyR5vTjJRAqpBYD28/ZLgXPf0pjG1VxOvtCV/BgXkQbpSe8Hw==", + "dev": true + }, "@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", diff --git a/src/package.json b/src/package.json index 5d403daab..ad5537947 100644 --- a/src/package.json +++ b/src/package.json @@ -68,17 +68,18 @@ "terser": "^5.27.0", "threads": "^1.7.0", "tinycon": "0.6.8", + "tsx": "^4.7.0", "ueberdb2": "^4.2.48", "underscore": "1.13.6", "unorm": "1.6.0", - "wtfnode": "^0.9.1", - "tsx": "^4.7.0" + "wtfnode": "^0.9.1" }, "bin": { "etherpad-healthcheck": "bin/etherpad-healthcheck", "etherpad-lite": "node/server.ts" }, "devDependencies": { + "@types/async": "^3.2.24", "typescript": "^5.3.3", "@types/node": "^20.11.5", "eslint": "^8.56.0", diff --git a/src/tsconfig.json b/src/tsconfig.json index e075f973c..5706def60 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ - + "moduleDetection": "force", + "lib": ["es6"], /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ @@ -11,7 +12,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */