diff --git a/src/node/db/API.js b/src/node/db/API.ts similarity index 90% rename from src/node/db/API.js rename to src/node/db/API.ts index b9efb5411..8b84693ec 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.ts @@ -104,7 +104,7 @@ Example returns: } */ -exports.getAttributePool = async (padID) => { +exports.getAttributePool = async (padID: string) => { const pad = await getPadSafe(padID, true); return {pool: pad.pool}; }; @@ -122,7 +122,7 @@ Example returns: } */ -exports.getRevisionChangeset = async (padID, rev) => { +exports.getRevisionChangeset = async (padID: string, rev: string) => { // try to parse the revision number if (rev !== undefined) { rev = checkValidRev(rev); @@ -155,7 +155,7 @@ Example returns: {code: 0, message:"ok", data: {text:"Welcome Text"}} {code: 1, message:"padID does not exist", data: null} */ -exports.getText = async (padID, rev) => { +exports.getText = async (padID: string, rev: string) => { // try to parse the revision number if (rev !== undefined) { rev = checkValidRev(rev); @@ -173,7 +173,7 @@ exports.getText = async (padID, rev) => { } // get the text of this revision - // getInternalRevisionAText() returns an atext object but we only want the .text inside it. + // getInternalRevisionAText() returns an atext object, but we only want the .text inside it. // Details at https://github.com/ether/etherpad-lite/issues/5073 const {text} = await pad.getInternalRevisionAText(rev); return {text}; @@ -200,7 +200,7 @@ Example returns: * @param {String} authorId the id of the author, defaulting to empty string * @returns {Promise} */ -exports.setText = async (padID, text, authorId = '') => { +exports.setText = async (padID: string, text?: string, authorId: string = ''): Promise => { // text is required if (typeof text !== 'string') { throw new CustomError('text is not a string', 'apierror'); @@ -225,7 +225,7 @@ Example returns: @param {String} text the text of the pad @param {String} authorId the id of the author, defaulting to empty string */ -exports.appendText = async (padID, text, authorId = '') => { +exports.appendText = async (padID:string, text?: string, authorId:string = '') => { // text is required if (typeof text !== 'string') { throw new CustomError('text is not a string', 'apierror'); @@ -247,7 +247,7 @@ Example returns: @param {String} rev the revision number, defaulting to the latest revision @return {Promise<{html: string}>} the html of the pad */ -exports.getHTML = async (padID, rev) => { +exports.getHTML = async (padID: string, rev: string): Promise<{ html: string; }> => { if (rev !== undefined) { rev = checkValidRev(rev); } @@ -283,7 +283,7 @@ Example returns: @param {String} html the html of the pad @param {String} authorId the id of the author, defaulting to empty string */ -exports.setHTML = async (padID, html, authorId = '') => { +exports.setHTML = async (padID: string, html:string|object, authorId = '') => { // html string is required if (typeof html !== 'string') { throw new CustomError('html is not a string', 'apierror'); @@ -324,7 +324,7 @@ Example returns: @param {Number} start the start point of the chat-history @param {Number} end the end point of the chat-history */ -exports.getChatHistory = async (padID, start, end) => { +exports.getChatHistory = async (padID: string, start:number, end:number) => { if (start && end) { if (start < 0) { throw new CustomError('start is below zero', 'apierror'); @@ -374,7 +374,7 @@ Example returns: @param {String} authorID the id of the author @param {Number} time the timestamp of the chat-message */ -exports.appendChatMessage = async (padID, text, authorID, time) => { +exports.appendChatMessage = async (padID: string, text: string|object, authorID: string, time: number) => { // text is required if (typeof text !== 'string') { throw new CustomError('text is not a string', 'apierror'); @@ -404,7 +404,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @param {String} padID the id of the pad */ -exports.getRevisionsCount = async (padID) => { +exports.getRevisionsCount = async (padID: string) => { // get the pad const pad = await getPadSafe(padID, true); return {revisions: pad.getHeadRevisionNumber()}; @@ -419,7 +419,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @param {String} padID the id of the pad */ -exports.getSavedRevisionsCount = async (padID) => { +exports.getSavedRevisionsCount = async (padID: string) => { // get the pad const pad = await getPadSafe(padID, true); return {savedRevisions: pad.getSavedRevisionsNumber()}; @@ -434,7 +434,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @param {String} padID the id of the pad */ -exports.listSavedRevisions = async (padID) => { +exports.listSavedRevisions = async (padID: string) => { // get the pad const pad = await getPadSafe(padID, true); return {savedRevisions: pad.getSavedRevisionsList()}; @@ -450,7 +450,7 @@ Example returns: @param {String} padID the id of the pad @param {Number} rev the revision number, defaulting to the latest revision */ -exports.saveRevision = async (padID, rev) => { +exports.saveRevision = async (padID: string, rev: number) => { // check if rev is a number if (rev !== undefined) { rev = checkValidRev(rev); @@ -483,7 +483,7 @@ Example returns: @param {String} padID the id of the pad @return {Promise<{lastEdited: number}>} the timestamp of the last revision of the pad */ -exports.getLastEdited = async (padID) => { +exports.getLastEdited = async (padID: string): Promise<{ lastEdited: number; }> => { // get the pad const pad = await getPadSafe(padID, true); const lastEdited = await pad.getLastEdit(); @@ -497,11 +497,11 @@ Example returns: {code: 0, message:"ok", data: null} {code: 1, message:"pad does already exist", data: null} - @param {String} padName the name of the new pad + @param {String} padID the name of the new pad @param {String} text the initial text of the pad @param {String} authorId the id of the author, defaulting to empty string */ -exports.createPad = async (padID, text, authorId = '') => { +exports.createPad = async (padID: string, text: string, authorId = '') => { if (padID) { // ensure there is no $ in the padID if (padID.indexOf('$') !== -1) { @@ -527,7 +527,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @param {String} padID the id of the pad */ -exports.deletePad = async (padID) => { +exports.deletePad = async (padID: string) => { const pad = await getPadSafe(padID, true); await pad.remove(); }; @@ -543,7 +543,7 @@ exports.deletePad = async (padID) => { @param {Number} rev the revision number, defaulting to the latest revision @param {String} authorId the id of the author, defaulting to empty string */ -exports.restoreRevision = async (padID, rev, authorId = '') => { +exports.restoreRevision = async (padID: string, rev: number, authorId = '') => { // check if rev is a number if (rev === undefined) { throw new CustomError('rev is not defined', 'apierror'); @@ -563,7 +563,7 @@ exports.restoreRevision = async (padID, rev, authorId = '') => { const oldText = pad.text(); atext.text += '\n'; - const eachAttribRun = (attribs, func) => { + const eachAttribRun = (attribs: string[], func:Function) => { let textIndex = 0; const newTextStart = 0; const newTextEnd = atext.text.length; @@ -580,7 +580,7 @@ exports.restoreRevision = async (padID, rev, authorId = '') => { const builder = Changeset.builder(oldText.length); // assemble each line into the builder - eachAttribRun(atext.attribs, (start, end, attribs) => { + eachAttribRun(atext.attribs, (start: number, end: number, attribs:string[]) => { builder.insert(atext.text.substring(start, end), attribs); }); @@ -610,7 +610,7 @@ Example returns: @param {String} destinationID the id of the destination pad @param {Boolean} force whether to overwrite the destination pad if it exists */ -exports.copyPad = async (sourceID, destinationID, force) => { +exports.copyPad = async (sourceID: string, destinationID: string, force: boolean) => { const pad = await getPadSafe(sourceID, true); await pad.copy(destinationID, force); }; @@ -628,7 +628,7 @@ Example returns: @param {Boolean} force whether to overwrite the destination pad if it exists @param {String} authorId the id of the author, defaulting to empty string */ -exports.copyPadWithoutHistory = async (sourceID, destinationID, force, authorId = '') => { +exports.copyPadWithoutHistory = async (sourceID: string, destinationID: string, force:boolean, authorId = '') => { const pad = await getPadSafe(sourceID, true); await pad.copyPadWithoutHistory(destinationID, force, authorId); }; @@ -645,7 +645,7 @@ Example returns: @param {String} destinationID the id of the destination pad @param {Boolean} force whether to overwrite the destination pad if it exists */ -exports.movePad = async (sourceID, destinationID, force) => { +exports.movePad = async (sourceID: string, destinationID: string, force:boolean) => { const pad = await getPadSafe(sourceID, true); await pad.copy(destinationID, force); await pad.remove(); @@ -660,7 +660,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @param {String} padID the id of the pad */ -exports.getReadOnlyID = async (padID) => { +exports.getReadOnlyID = async (padID: string) => { // we don't need the pad object, but this function does all the security stuff for us await getPadSafe(padID, true); @@ -679,7 +679,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @param {String} roID the readonly id of the pad */ -exports.getPadID = async (roID) => { +exports.getPadID = async (roID: string) => { // get the PadId const padID = await readOnlyManager.getPadId(roID); if (padID == null) { @@ -699,7 +699,7 @@ Example returns: @param {String} padID the id of the pad @param {Boolean} publicStatus the public status of the pad */ -exports.setPublicStatus = async (padID, publicStatus) => { +exports.setPublicStatus = async (padID: string, publicStatus: boolean|string) => { // ensure this is a group pad checkGroupPad(padID, 'publicStatus'); @@ -723,7 +723,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @param {String} padID the id of the pad */ -exports.getPublicStatus = async (padID) => { +exports.getPublicStatus = async (padID: string) => { // ensure this is a group pad checkGroupPad(padID, 'publicStatus'); @@ -741,7 +741,7 @@ Example returns: {code: 1, message:"padID does not exist", data: null} @param {String} padID the id of the pad */ -exports.listAuthorsOfPad = async (padID) => { +exports.listAuthorsOfPad = async (padID: string) => { // get the pad const pad = await getPadSafe(padID, true); const authorIDs = pad.getAllAuthors(); @@ -773,7 +773,7 @@ Example returns: @param {String} msg the message to send */ -exports.sendClientsMessage = async (padID, msg) => { +exports.sendClientsMessage = async (padID: string, msg: string) => { await getPadSafe(padID, true); // Throw if the padID is invalid or if the pad does not exist. padMessageHandler.handleCustomMessage(padID, msg); }; @@ -799,7 +799,7 @@ Example returns: @param {String} padID the id of the pad @return {Promise<{chatHead: number}>} the chatHead of the pad */ -exports.getChatHead = async (padID) => { +exports.getChatHead = async (padID:string): Promise<{ chatHead: number; }> => { // get the pad const pad = await getPadSafe(padID, true); return {chatHead: pad.chatHead}; @@ -825,7 +825,7 @@ Example returns: @param {Number} startRev the start revision number @param {Number} endRev the end revision number */ -exports.createDiffHTML = async (padID, startRev, endRev) => { +exports.createDiffHTML = async (padID: string, startRev: number, endRev: number) => { // check if startRev is a number if (startRev !== undefined) { startRev = checkValidRev(startRev); @@ -846,7 +846,7 @@ exports.createDiffHTML = async (padID, startRev, endRev) => { let padDiff; try { padDiff = new PadDiff(pad, startRev, endRev); - } catch (e) { + } catch (e:any) { throw {stop: e.message}; } @@ -872,6 +872,7 @@ exports.getStats = async () => { const sessionInfos = padMessageHandler.sessioninfos; const sessionKeys = Object.keys(sessionInfos); + // @ts-ignore const activePads = new Set(Object.entries(sessionInfos).map((k) => k[1].padId)); const {padIDs} = await padManager.listAllPads(); @@ -888,7 +889,7 @@ exports.getStats = async () => { **************************** */ // gets a pad safe -const getPadSafe = async (padID, shouldExist, text, authorId = '') => { +const getPadSafe = async (padID: string|object, shouldExist: boolean, text?:string, authorId:string = '') => { // check if padID is a string if (typeof padID !== 'string') { throw new CustomError('padID is not a string', 'apierror'); @@ -917,7 +918,7 @@ const getPadSafe = async (padID, shouldExist, text, authorId = '') => { }; // checks if a padID is part of a group -const checkGroupPad = (padID, field) => { +const checkGroupPad = (padID: string, field: string) => { // ensure this is a group pad if (padID && padID.indexOf('$') === -1) { throw new CustomError( diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.ts similarity index 86% rename from src/node/db/AuthorManager.js rename to src/node/db/AuthorManager.ts index 7413b5806..2f4e7d751 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.ts @@ -95,7 +95,7 @@ exports.getColorPalette = () => [ * Checks if the author exists * @param {String} authorID The id of the author */ -exports.doesAuthorExist = async (authorID) => { +exports.doesAuthorExist = async (authorID: string) => { const author = await db.get(`globalAuthor:${authorID}`); return author != null; @@ -114,7 +114,7 @@ exports.doesAuthorExists = exports.doesAuthorExist; * @param {String} mapperkey The database key name for this mapper * @param {String} mapper The mapper */ -const mapAuthorWithDBKey = async (mapperkey, mapper) => { +const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => { // try to map to an author const author = await db.get(`${mapperkey}:${mapper}`); @@ -142,7 +142,7 @@ const mapAuthorWithDBKey = async (mapperkey, mapper) => { * @param {String} token The token of the author * @return {Promise} */ -const getAuthor4Token = async (token) => { +const getAuthor4Token = async (token: string) => { const author = await mapAuthorWithDBKey('token2author', token); // return only the sub value authorID @@ -155,7 +155,7 @@ const getAuthor4Token = async (token) => { * @param {Object} user * @return {Promise<*>} */ -exports.getAuthorId = async (token, user) => { +exports.getAuthorId = async (token: string, user: object) => { const context = {dbKey: token, token, user}; let [authorId] = await hooks.aCallFirst('getAuthorId', context); if (!authorId) authorId = await getAuthor4Token(context.dbKey); @@ -168,7 +168,7 @@ exports.getAuthorId = async (token, user) => { * @deprecated Use `getAuthorId` instead. * @param {String} token The token */ -exports.getAuthor4Token = async (token) => { +exports.getAuthor4Token = async (token: string) => { warnDeprecated( 'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead'); return await getAuthor4Token(token); @@ -179,7 +179,7 @@ exports.getAuthor4Token = async (token) => { * @param {String} authorMapper The mapper * @param {String} name The name of the author (optional) */ -exports.createAuthorIfNotExistsFor = async (authorMapper, name) => { +exports.createAuthorIfNotExistsFor = async (authorMapper: string, name: string) => { const author = await mapAuthorWithDBKey('mapper2author', authorMapper); if (name) { @@ -195,7 +195,7 @@ exports.createAuthorIfNotExistsFor = async (authorMapper, name) => { * Internal function that creates the database entry for an author * @param {String} name The name of the author */ -exports.createAuthor = async (name) => { +exports.createAuthor = async (name: string) => { // create the new author name const author = `a.${randomString(16)}`; @@ -216,41 +216,41 @@ exports.createAuthor = async (name) => { * Returns the Author Obj of the author * @param {String} author The id of the author */ -exports.getAuthor = async (author) => await db.get(`globalAuthor:${author}`); +exports.getAuthor = async (author: string) => await db.get(`globalAuthor:${author}`); /** * Returns the color Id of the author * @param {String} author The id of the author */ -exports.getAuthorColorId = async (author) => await db.getSub(`globalAuthor:${author}`, ['colorId']); +exports.getAuthorColorId = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['colorId']); /** * Sets the color Id of the author * @param {String} author The id of the author * @param {String} colorId The color id of the author */ -exports.setAuthorColorId = async (author, colorId) => await db.setSub( +exports.setAuthorColorId = async (author: string, colorId: string) => await db.setSub( `globalAuthor:${author}`, ['colorId'], colorId); /** * Returns the name of the author * @param {String} author The id of the author */ -exports.getAuthorName = async (author) => await db.getSub(`globalAuthor:${author}`, ['name']); +exports.getAuthorName = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['name']); /** * Sets the name of the author * @param {String} author The id of the author * @param {String} name The name of the author */ -exports.setAuthorName = async (author, name) => await db.setSub( +exports.setAuthorName = async (author: string, name: string) => await db.setSub( `globalAuthor:${author}`, ['name'], name); /** * Returns an array of all pads this author contributed to * @param {String} authorID The id of the author */ -exports.listPadsOfAuthor = async (authorID) => { +exports.listPadsOfAuthor = async (authorID: string) => { /* There are two other places where this array is manipulated: * (1) When the author is added to a pad, the author object is also updated * (2) When a pad is deleted, each author of that pad is also updated @@ -275,7 +275,7 @@ exports.listPadsOfAuthor = async (authorID) => { * @param {String} authorID The id of the author * @param {String} padID The id of the pad the author contributes to */ -exports.addPad = async (authorID, padID) => { +exports.addPad = async (authorID: string, padID: string) => { // get the entry const author = await db.get(`globalAuthor:${authorID}`); @@ -302,7 +302,7 @@ exports.addPad = async (authorID, padID) => { * @param {String} authorID The id of the author * @param {String} padID The id of the pad the author contributes to */ -exports.removePad = async (authorID, padID) => { +exports.removePad = async (authorID: string, padID: string) => { const author = await db.get(`globalAuthor:${authorID}`); if (author == null) return; diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.ts similarity index 93% rename from src/node/db/GroupManager.js rename to src/node/db/GroupManager.ts index 2ab20ac16..0524c4eda 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.ts @@ -42,7 +42,7 @@ exports.listAllGroups = async () => { * @param {String} groupID The id of the group * @return {Promise} Resolves when the group is deleted */ -exports.deleteGroup = async (groupID) => { +exports.deleteGroup = async (groupID: string): Promise => { const group = await db.get(`group:${groupID}`); // ensure group exists @@ -82,7 +82,7 @@ exports.deleteGroup = async (groupID) => { * @param {String} groupID the id of the group to delete * @return {Promise} Resolves to true if the group exists */ -exports.doesGroupExist = async (groupID) => { +exports.doesGroupExist = async (groupID: string) => { // try to get the group entry const group = await db.get(`group:${groupID}`); @@ -108,7 +108,7 @@ exports.createGroup = async () => { * @param groupMapper the mapper of the group * @return {Promise<{groupID: string}|{groupID: *}>} a promise that resolves to the group ID */ -exports.createGroupIfNotExistsFor = async (groupMapper) => { +exports.createGroupIfNotExistsFor = async (groupMapper: string|object) => { if (typeof groupMapper !== 'string') { throw new CustomError('groupMapper is not a string', 'apierror'); } @@ -134,7 +134,7 @@ exports.createGroupIfNotExistsFor = async (groupMapper) => { * @param {String} authorId The id of the author * @return {Promise<{padID: string}>} a promise that resolves to the id of the new pad */ -exports.createGroupPad = async (groupID, padName, text, authorId = '') => { +exports.createGroupPad = async (groupID: string, padName: string, text: string, authorId: string = ''): Promise<{ padID: string; }> => { // create the padID const padID = `${groupID}$${padName}`; @@ -167,7 +167,7 @@ exports.createGroupPad = async (groupID, padName, text, authorId = '') => { * @param {String} groupID The id of the group * @return {Promise<{padIDs: string[]}>} a promise that resolves to the ids of all pads of the group */ -exports.listPads = async (groupID) => { +exports.listPads = async (groupID: string): Promise<{ padIDs: string[]; }> => { const exists = await exports.doesGroupExist(groupID); // ensure the group exists diff --git a/src/node/db/Pad.js b/src/node/db/Pad.ts similarity index 89% rename from src/node/db/Pad.js rename to src/node/db/Pad.ts index 884b420f0..fa4af994d 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.ts @@ -1,4 +1,8 @@ 'use strict'; +import {Database} from "ueberdb2"; +import {AChangeSet, APool, AText} from "../types/PadType"; +import {MapArrayType} from "../types/MapType"; + /** * The pad object, defined with joose */ @@ -28,20 +32,29 @@ const promises = require('../utils/promises'); * @param {String} txt The text to clean * @returns {String} The cleaned text */ -exports.cleanText = (txt) => txt.replace(/\r\n/g, '\n') +exports.cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n') .replace(/\r/g, '\n') .replace(/\t/g, ' ') .replace(/\xa0/g, ' '); class Pad { + private db: Database; + private atext: AText; + private pool: APool; + private head: number; + private chatHead: number; + private publicStatus: boolean; + private id: string; + private savedRevisions: any[]; /** + * @param id * @param [database] - Database object to access this pad's records (and only this pad's records; * the shared global Etherpad database object is still used for all other pad accesses, such * as copying the pad). Defaults to the shared global Etherpad database object. This parameter * can be used to shard pad storage across multiple database backends, to put each pad in its * own database table, or to validate imported pad data before it is written to the database. */ - constructor(id, database = db) { + constructor(id:string, database = db) { this.db = database; this.atext = Changeset.makeAText('\n'); this.pool = new AttributePool(); @@ -80,7 +93,7 @@ class Pad { * @param {String} authorId The id of the author * @return {Promise} */ - async appendRevision(aChangeset, authorId = '') { + async appendRevision(aChangeset:AChangeSet, authorId = '') { const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool); if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs && this.head !== -1) { @@ -95,6 +108,7 @@ class Pad { const hook = this.head === 0 ? 'padCreate' : 'padUpdate'; await Promise.all([ + // @ts-ignore this.db.set(`pad:${this.id}:revs:${newRev}`, { changeset: aChangeset, meta: { @@ -129,32 +143,39 @@ class Pad { } toJSON() { - const o = {...this, pool: this.pool.toJsonable()}; + const o:Pad = {...this, pool: this.pool.toJsonable()}; + // @ts-ignore delete o.db; + // @ts-ignore delete o.id; return o; } // save all attributes to the database async saveToDatabase() { + // @ts-ignore await this.db.set(`pad:${this.id}`, this); } // get time of last edit (changeset application) async getLastEdit() { const revNum = this.getHeadRevisionNumber(); + // @ts-ignore return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']); } - async getRevisionChangeset(revNum) { + async getRevisionChangeset(revNum: number) { + // @ts-ignore return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['changeset']); } - async getRevisionAuthor(revNum) { + async getRevisionAuthor(revNum: number) { + // @ts-ignore return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'author']); } - async getRevisionDate(revNum) { + async getRevisionDate(revNum: number) { + // @ts-ignore return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']); } @@ -162,7 +183,8 @@ class Pad { * @param {number} revNum - Must be a key revision number (see `getKeyRevisionNumber`). * @returns The attribute text stored at `revNum`. */ - async _getKeyRevisionAText(revNum) { + async _getKeyRevisionAText(revNum: number) { + // @ts-ignore return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'atext']); } @@ -182,7 +204,7 @@ class Pad { return authorIds; } - async getInternalRevisionAText(targetRev) { + async getInternalRevisionAText(targetRev: number) { const keyRev = this.getKeyRevisionNumber(targetRev); const headRev = this.getHeadRevisionNumber(); if (targetRev > headRev) targetRev = headRev; @@ -197,17 +219,17 @@ class Pad { return atext; } - async getRevision(revNum) { + async getRevision(revNum: number) { return await this.db.get(`pad:${this.id}:revs:${revNum}`); } async getAllAuthorColors() { const authorIds = this.getAllAuthors(); - const returnTable = {}; + const returnTable:MapArrayType = {}; const colorPalette = authorManager.getColorPalette(); await Promise.all( - authorIds.map((authorId) => authorManager.getAuthorColorId(authorId).then((colorId) => { + authorIds.map((authorId) => authorManager.getAuthorColorId(authorId).then((colorId:string) => { // colorId might be a hex color or an number out of the palette returnTable[authorId] = colorPalette[colorId] || colorId; }))); @@ -215,7 +237,7 @@ class Pad { return returnTable; } - getValidRevisionRange(startRev, endRev) { + getValidRevisionRange(startRev: any, endRev:any) { startRev = parseInt(startRev, 10); const head = this.getHeadRevisionNumber(); endRev = endRev ? parseInt(endRev, 10) : head; @@ -236,14 +258,14 @@ class Pad { return null; } - getKeyRevisionNumber(revNum) { + getKeyRevisionNumber(revNum: number) { return Math.floor(revNum / 100) * 100; } /** * @returns {string} The pad's text. */ - text() { + text(): string { return this.atext.text; } @@ -258,7 +280,7 @@ class Pad { * @param {string} ins - New text to insert at `start` (after the `ndel` characters are deleted). * @param {string} [authorId] - Author ID of the user making the change (if applicable). */ - async spliceText(start, ndel, ins, authorId = '') { + async spliceText(start:number, ndel:number, ins: string, authorId: string = '') { if (start < 0) throw new RangeError(`start index must be non-negative (is ${start})`); if (ndel < 0) throw new RangeError(`characters to delete must be non-negative (is ${ndel})`); const orig = this.text(); @@ -283,7 +305,7 @@ class Pad { * @param {string} [authorId] - The author ID of the user that initiated the change, if * applicable. */ - async setText(newText, authorId = '') { + async setText(newText: string, authorId = '') { await this.spliceText(0, this.text().length, newText, authorId); } @@ -294,7 +316,7 @@ class Pad { * @param {string} [authorId] - The author ID of the user that initiated the change, if * applicable. */ - async appendText(newText, authorId = '') { + async appendText(newText:string, authorId = '') { await this.spliceText(this.text().length - 1, 0, newText, authorId); } @@ -308,7 +330,7 @@ class Pad { * @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use * `msgOrText.time` instead. */ - async appendChatMessage(msgOrText, authorId = null, time = null) { + async appendChatMessage(msgOrText: string|typeof ChatMessage, authorId = null, time = null) { const msg = msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time); this.chatHead++; @@ -325,7 +347,7 @@ class Pad { * @param {number} entryNum - ID of the desired chat message. * @returns {?ChatMessage} */ - async getChatMessage(entryNum) { + async getChatMessage(entryNum: number) { const entry = await this.db.get(`pad:${this.id}:chat:${entryNum}`); if (entry == null) return null; const message = ChatMessage.fromObject(entry); @@ -340,7 +362,7 @@ class Pad { * (inclusive), in order. Note: `start` and `end` form a closed interval, not a half-open * interval as is typical in code. */ - async getChatMessages(start, end) { + async getChatMessages(start: string, end: number) { const entries = await Promise.all(Stream.range(start, end + 1).map(this.getChatMessage.bind(this))); @@ -356,7 +378,7 @@ class Pad { }); } - async init(text, authorId = '') { + async init(text:string, authorId = '') { // try to load the pad const value = await this.db.get(`pad:${this.id}`); @@ -377,7 +399,7 @@ class Pad { await hooks.aCallAll('padLoad', {pad: this}); } - async copy(destinationID, force) { + async copy(destinationID: string, force: boolean) { // Kick everyone from this pad. // This was commented due to https://github.com/ether/etherpad-lite/issues/3183. // Do we really need to kick everyone out? @@ -392,15 +414,18 @@ class Pad { // if force is true and already exists a Pad with the same id, remove that Pad await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force); - const copyRecord = async (keySuffix) => { + const copyRecord = async (keySuffix: string) => { const val = await this.db.get(`pad:${this.id}${keySuffix}`); await db.set(`pad:${destinationID}${keySuffix}`, val); }; const promises = (function* () { yield copyRecord(''); + // @ts-ignore yield* Stream.range(0, this.head + 1).map((i) => copyRecord(`:revs:${i}`)); + // @ts-ignore yield* Stream.range(0, this.chatHead + 1).map((i) => copyRecord(`:chat:${i}`)); + // @ts-ignore yield this.copyAuthorInfoToDestinationPad(destinationID); if (destGroupID) yield db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1); }).call(this); @@ -427,8 +452,8 @@ class Pad { return {padID: destinationID}; } - async checkIfGroupExistAndReturnIt(destinationID) { - let destGroupID = false; + async checkIfGroupExistAndReturnIt(destinationID: string) { + let destGroupID:false|string = false; if (destinationID.indexOf('$') >= 0) { destGroupID = destinationID.split('$')[0]; @@ -442,7 +467,7 @@ class Pad { return destGroupID; } - async removePadIfForceIsTrueAndAlreadyExist(destinationID, force) { + async removePadIfForceIsTrueAndAlreadyExist(destinationID: string, force: boolean|string) { // if the pad exists, we should abort, unless forced. const exists = await padManager.doesPadExist(destinationID); @@ -465,13 +490,13 @@ class Pad { } } - async copyAuthorInfoToDestinationPad(destinationID) { + async copyAuthorInfoToDestinationPad(destinationID: string) { // add the new sourcePad to all authors who contributed to the old one await Promise.all(this.getAllAuthors().map( (authorID) => authorManager.addPad(authorID, destinationID))); } - async copyPadWithoutHistory(destinationID, force, authorId = '') { + async copyPadWithoutHistory(destinationID: string, force: string|boolean, authorId = '') { // flush the source pad this.saveToDatabase(); @@ -554,18 +579,18 @@ class Pad { } // remove the readonly entries - p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID) => { + p.push(readOnlyManager.getReadOnlyId(padID).then(async (readonlyID: string) => { await db.remove(`readonly2pad:${readonlyID}`); })); p.push(db.remove(`pad2readonly:${padID}`)); // delete all chat messages - p.push(promises.timesLimit(this.chatHead + 1, 500, async (i) => { + p.push(promises.timesLimit(this.chatHead + 1, 500, async (i: string) => { await this.db.remove(`pad:${this.id}:chat:${i}`, null); })); // delete all revisions - p.push(promises.timesLimit(this.head + 1, 500, async (i) => { + p.push(promises.timesLimit(this.head + 1, 500, async (i: string) => { await this.db.remove(`pad:${this.id}:revs:${i}`, null); })); @@ -587,12 +612,12 @@ class Pad { } // set in db - async setPublicStatus(publicStatus) { + async setPublicStatus(publicStatus: boolean) { this.publicStatus = publicStatus; await this.saveToDatabase(); } - async addSavedRevision(revNum, savedById, label) { + async addSavedRevision(revNum: string, savedById: string, label: string) { // if this revision is already saved, return silently for (const i in this.savedRevisions) { if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) { @@ -601,7 +626,7 @@ class Pad { } // build the saved revision object - const savedRevision = {}; + const savedRevision:MapArrayType = {}; savedRevision.revNum = revNum; savedRevision.savedById = savedById; savedRevision.label = label || `Revision ${revNum}`; @@ -664,7 +689,7 @@ class Pad { if (k === 'author' && v) authorIds.add(v); }); const revs = Stream.range(0, head + 1) - .map(async (r) => { + .map(async (r: number) => { const isKeyRev = r === this.getKeyRevisionNumber(r); try { return await Promise.all([ @@ -675,7 +700,7 @@ class Pad { isKeyRev, isKeyRev ? this._getKeyRevisionAText(r) : null, ]); - } catch (err) { + } catch (err:any) { err.message = `(pad ${this.id} revision ${r}) ${err.message}`; throw err; } @@ -708,7 +733,7 @@ class Pad { } atext = Changeset.applyToAText(changeset, atext, pool); if (isKeyRev) assert.deepEqual(keyAText, atext); - } catch (err) { + } catch (err:any) { err.message = `(pad ${this.id} revision ${r}) ${err.message}`; throw err; } @@ -721,12 +746,12 @@ class Pad { assert(Number.isInteger(this.chatHead)); assert(this.chatHead >= -1); const chats = Stream.range(0, this.chatHead + 1) - .map(async (c) => { + .map(async (c: number) => { try { const msg = await this.getChatMessage(c); assert(msg != null); assert(msg instanceof ChatMessage); - } catch (err) { + } catch (err:any) { err.message = `(pad ${this.id} chat message ${c}) ${err.message}`; throw err; } diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.ts similarity index 86% rename from src/node/db/PadManager.js rename to src/node/db/PadManager.ts index 7c4021541..4a447f850 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.ts @@ -19,6 +19,8 @@ * limitations under the License. */ +import {MapArrayType} from "../types/MapType"; + const CustomError = require('../utils/customError'); const Pad = require('../db/Pad'); const db = require('./DB'); @@ -35,12 +37,16 @@ const settings = require('../utils/Settings'); * If this is needed in other places, it would be wise to make this a prototype * that's defined somewhere more sensible. */ -const globalPads = { - get(name) { return this[`:${name}`]; }, - set(name, value) { +const globalPads:MapArrayType = { + get(name: string) + { + return this[`:${name}`]; + }, + set(name: string, value: any) + { this[`:${name}`] = value; }, - remove(name) { + remove(name: string) { delete this[`:${name}`]; }, }; @@ -51,6 +57,9 @@ const globalPads = { * Updated without db access as new pads are created/old ones removed. */ const padList = new class { + private _cachedList: string[] | null; + private _list: Set; + private _loaded: Promise | null; constructor() { this._cachedList = null; this._list = new Set(); @@ -74,13 +83,13 @@ const padList = new class { return this._cachedList; } - addPad(name) { + addPad(name: string) { if (this._list.has(name)) return; this._list.add(name); this._cachedList = null; } - removePad(name) { + removePad(name: string) { if (!this._list.has(name)) return; this._list.delete(name); this._cachedList = null; @@ -96,7 +105,7 @@ const padList = new class { * @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if * applicable). */ -exports.getPad = async (id, text, authorId = '') => { +exports.getPad = async (id: string, text: string, authorId:string = '') => { // check if this is a valid padId if (!exports.isValidPadId(id)) { throw new CustomError(`${id} is not a valid padId`, 'apierror'); @@ -140,7 +149,7 @@ exports.listAllPads = async () => { }; // checks if a pad exists -exports.doesPadExist = async (padId) => { +exports.doesPadExist = async (padId: string) => { const value = await db.get(`pad:${padId}`); return (value != null && value.atext); @@ -159,7 +168,7 @@ const padIdTransforms = [ ]; // returns a sanitized padId, respecting legacy pad id formats -exports.sanitizePadId = async (padId) => { +exports.sanitizePadId = async (padId: string) => { for (let i = 0, n = padIdTransforms.length; i < n; ++i) { const exists = await exports.doesPadExist(padId); @@ -169,6 +178,7 @@ exports.sanitizePadId = async (padId) => { const [from, to] = padIdTransforms[i]; + // @ts-ignore padId = padId.replace(from, to); } @@ -178,12 +188,12 @@ exports.sanitizePadId = async (padId) => { return padId; }; -exports.isValidPadId = (padId) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId); +exports.isValidPadId = (padId: string) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId); /** * Removes the pad from database and unloads it. */ -exports.removePad = async (padId) => { +exports.removePad = async (padId: string) => { const p = db.remove(`pad:${padId}`); exports.unloadPad(padId); padList.removePad(padId); @@ -191,6 +201,6 @@ exports.removePad = async (padId) => { }; // removes a pad from the cache -exports.unloadPad = (padId) => { +exports.unloadPad = (padId: string) => { globalPads.remove(padId); }; diff --git a/src/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.ts similarity index 89% rename from src/node/db/ReadOnlyManager.js rename to src/node/db/ReadOnlyManager.ts index b96dd3413..23639d665 100644 --- a/src/node/db/ReadOnlyManager.js +++ b/src/node/db/ReadOnlyManager.ts @@ -29,14 +29,14 @@ const randomString = require('../utils/randomstring'); * @param {String} id the pad's id * @return {Boolean} true if the id is readonly */ -exports.isReadOnlyId = (id) => id.startsWith('r.'); +exports.isReadOnlyId = (id:string) => id.startsWith('r.'); /** * returns a read only id for a pad * @param {String} padId the id of the pad * @return {String} the read only id */ -exports.getReadOnlyId = async (padId) => { +exports.getReadOnlyId = async (padId:string) => { // check if there is a pad2readonly entry let readOnlyId = await db.get(`pad2readonly:${padId}`); @@ -57,14 +57,14 @@ exports.getReadOnlyId = async (padId) => { * @param {String} readOnlyId read only id * @return {String} the padId */ -exports.getPadId = async (readOnlyId) => await db.get(`readonly2pad:${readOnlyId}`); +exports.getPadId = async (readOnlyId:string) => await db.get(`readonly2pad:${readOnlyId}`); /** * returns the padId and readonlyPadId in an object for any id * @param {String} id read only id or real pad id * @return {Object} an object with the padId and readonlyPadId */ -exports.getIds = async (id) => { +exports.getIds = async (id:string) => { const readonly = exports.isReadOnlyId(id); // Might be null, if this is an unknown read-only id diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.ts similarity index 96% rename from src/node/db/SecurityManager.js rename to src/node/db/SecurityManager.ts index 0e5b2c77c..326bf3659 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.ts @@ -19,6 +19,8 @@ * limitations under the License. */ +import {UserSettingsObject} from "../types/UserSettingsObject"; + const authorManager = require('./AuthorManager'); const hooks = require('../../static/js/pluginfw/hooks.js'); const padManager = require('./PadManager'); @@ -55,7 +57,7 @@ const DENY = Object.freeze({accessStatus: 'deny'}); * @param {Object} userSettings * @return {DENY|{accessStatus: String, authorID: String}} */ -exports.checkAccess = async (padID, sessionCookie, token, userSettings) => { +exports.checkAccess = async (padID:string, sessionCookie:string, token:string, userSettings:UserSettingsObject) => { if (!padID) { authLogger.debug('access denied: missing padID'); return DENY; @@ -95,7 +97,7 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => { } // allow plugins to deny access - const isFalse = (x) => x === false; + const isFalse = (x:boolean) => x === false; if (hooks.callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) { authLogger.debug('access denied: an onAccessCheck hook function returned false'); return DENY; diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.ts similarity index 91% rename from src/node/db/SessionManager.js rename to src/node/db/SessionManager.ts index 3d39ed747..c0e43a659 100644 --- a/src/node/db/SessionManager.js +++ b/src/node/db/SessionManager.ts @@ -36,7 +36,7 @@ const authorManager = require('./AuthorManager'); * sessionCookie, and is bound to a group with the given ID, then this returns the author ID * bound to the session. Otherwise, returns undefined. */ -exports.findAuthorID = async (groupID, sessionCookie) => { +exports.findAuthorID = async (groupID:string, sessionCookie: string) => { if (!sessionCookie) return undefined; /* * Sometimes, RFC 6265-compliant web servers may send back a cookie whose @@ -65,7 +65,7 @@ exports.findAuthorID = async (groupID, sessionCookie) => { const sessionInfoPromises = sessionIDs.map(async (id) => { try { return await exports.getSessionInfo(id); - } catch (err) { + } catch (err:any) { if (err.message === 'sessionID does not exist') { console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`); } else { @@ -75,7 +75,10 @@ exports.findAuthorID = async (groupID, sessionCookie) => { return undefined; }); const now = Math.floor(Date.now() / 1000); - const isMatch = (si) => (si != null && si.groupID === groupID && now < si.validUntil); + const isMatch = (si: { + groupID: string; + validUntil: number; + }|null) => (si != null && si.groupID === groupID && now < si.validUntil); const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch); if (sessionInfo == null) return undefined; return sessionInfo.authorID; @@ -86,7 +89,7 @@ exports.findAuthorID = async (groupID, sessionCookie) => { * @param {String} sessionID The id of the session * @return {Promise} Resolves to true if the session exists */ -exports.doesSessionExist = async (sessionID) => { +exports.doesSessionExist = async (sessionID: string) => { // check if the database entry of this session exists const session = await db.get(`session:${sessionID}`); return (session != null); @@ -99,7 +102,7 @@ exports.doesSessionExist = async (sessionID) => { * @param {Number} validUntil The unix timestamp when the session should expire * @return {Promise<{sessionID: string}>} the id of the new session */ -exports.createSession = async (groupID, authorID, validUntil) => { +exports.createSession = async (groupID: string, authorID: string, validUntil: number) => { // check if the group exists const groupExists = await groupManager.doesGroupExist(groupID); if (!groupExists) { @@ -160,7 +163,7 @@ exports.createSession = async (groupID, authorID, validUntil) => { * @param {String} sessionID The id of the session * @return {Promise} the sessioninfos */ -exports.getSessionInfo = async (sessionID) => { +exports.getSessionInfo = async (sessionID:string) => { // check if the database entry of this session exists const session = await db.get(`session:${sessionID}`); @@ -178,7 +181,7 @@ exports.getSessionInfo = async (sessionID) => { * @param {String} sessionID The id of the session * @return {Promise} Resolves when the session is deleted */ -exports.deleteSession = async (sessionID) => { +exports.deleteSession = async (sessionID:string) => { // ensure that the session exists const session = await db.get(`session:${sessionID}`); if (session == null) { @@ -207,7 +210,7 @@ exports.deleteSession = async (sessionID) => { * @param {String} groupID The id of the group * @return {Promise} The sessioninfos of all sessions of this group */ -exports.listSessionsOfGroup = async (groupID) => { +exports.listSessionsOfGroup = async (groupID: string) => { // check that the group exists const exists = await groupManager.doesGroupExist(groupID); if (!exists) { @@ -223,7 +226,7 @@ exports.listSessionsOfGroup = async (groupID) => { * @param {String} authorID The id of the author * @return {Promise} The sessioninfos of all sessions of this author */ -exports.listSessionsOfAuthor = async (authorID) => { +exports.listSessionsOfAuthor = async (authorID: string) => { // check that the author exists const exists = await authorManager.doesAuthorExist(authorID); if (!exists) { @@ -240,7 +243,7 @@ exports.listSessionsOfAuthor = async (authorID) => { * @param {String} dbkey The db key to use to get the sessions * @return {Promise<*>} */ -const listSessionsWithDBKey = async (dbkey) => { +const listSessionsWithDBKey = async (dbkey: string) => { // get the group2sessions entry const sessionObject = await db.get(dbkey); const sessions = sessionObject ? sessionObject.sessionIDs : null; @@ -249,7 +252,7 @@ const listSessionsWithDBKey = async (dbkey) => { for (const sessionID of Object.keys(sessions || {})) { try { sessions[sessionID] = await exports.getSessionInfo(sessionID); - } catch (err) { + } catch (err:any) { if (err.name === 'apierror') { console.warn(`Found bad session ${sessionID} in ${dbkey}`); sessions[sessionID] = null; @@ -262,9 +265,11 @@ const listSessionsWithDBKey = async (dbkey) => { return sessions; }; + /** * checks if a number is an int * @param {number|string} value * @return {boolean} If the value is an integer */ -const isInt = (value) => (parseFloat(value) === parseInt(value)) && !isNaN(value); +// @ts-ignore +const isInt = (value:number|string): boolean => (parseFloat(value) === parseInt(value)) && !isNaN(value); diff --git a/src/node/eejs/index.js b/src/node/eejs/index.ts similarity index 90% rename from src/node/eejs/index.js rename to src/node/eejs/index.ts index 31ff2c1a5..eb9e882f0 100644 --- a/src/node/eejs/index.js +++ b/src/node/eejs/index.ts @@ -38,16 +38,16 @@ exports.info = { const getCurrentFile = () => exports.info.file_stack[exports.info.file_stack.length - 1]; -exports._init = (b, recursive) => { +exports._init = (b: any, recursive: boolean) => { exports.info.__output_stack.push(exports.info.__output); exports.info.__output = b; }; -exports._exit = (b, recursive) => { +exports._exit = (b:any, recursive:boolean) => { exports.info.__output = exports.info.__output_stack.pop(); }; -exports.begin_block = (name) => { +exports.begin_block = (name:string) => { exports.info.block_stack.push(name); exports.info.__output_stack.push(exports.info.__output.get()); exports.info.__output.set(''); @@ -63,11 +63,17 @@ exports.end_block = () => { exports.info.__output.set(exports.info.__output.get().concat(args.content)); }; -exports.require = (name, args, mod) => { +exports.require = (name:string, args:{ + e?: Function, + require?: Function, +}, mod:{ + filename:string, + paths:string[], +}) => { if (args == null) args = {}; let basedir = __dirname; - let paths = []; + let paths:string[] = []; if (exports.info.file_stack.length) { basedir = path.dirname(getCurrentFile().path); diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.ts similarity index 82% rename from src/node/hooks/express/specialpages.js rename to src/node/hooks/express/specialpages.ts index 18a17988a..85a23479f 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.ts @@ -10,10 +10,10 @@ const settings = require('../../utils/Settings'); const util = require('util'); const webaccess = require('./webaccess'); -exports.expressPreSession = async (hookName, {app}) => { +exports.expressPreSession = async (hookName:string, {app}:any) => { // This endpoint is intended to conform to: // https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html - app.get('/health', (req, res) => { + app.get('/health', (req:any, res:any) => { res.set('Content-Type', 'application/health+json'); res.json({ status: 'pass', @@ -21,18 +21,18 @@ exports.expressPreSession = async (hookName, {app}) => { }); }); - app.get('/stats', (req, res) => { + app.get('/stats', (req:any, res:any) => { res.json(require('../../stats').toJSON()); }); - app.get('/javascript', (req, res) => { + app.get('/javascript', (req:any, res:any) => { res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req})); }); - app.get('/robots.txt', (req, res) => { + app.get('/robots.txt', (req:any, res:any) => { let filePath = path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt'); - res.sendFile(filePath, (err) => { + res.sendFile(filePath, (err:any) => { // there is no custom robots.txt, send the default robots.txt which dissallows all if (err) { filePath = path.join(settings.root, 'src', 'static', 'robots.txt'); @@ -41,7 +41,7 @@ exports.expressPreSession = async (hookName, {app}) => { }); }); - app.get('/favicon.ico', (req, res, next) => { + app.get('/favicon.ico', (req:any, res:any, next:Function) => { (async () => { /* If this is a url we simply redirect to that one. @@ -73,14 +73,14 @@ exports.expressPreSession = async (hookName, {app}) => { }); }; -exports.expressCreateServer = (hookName, args, cb) => { +exports.expressCreateServer = (hookName:string, args:any, cb:Function) => { // serve index.html under / - args.app.get('/', (req, res) => { + args.app.get('/', (req:any, res:any) => { res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req})); }); // serve pad.html under /p - args.app.get('/p/:pad', (req, res, next) => { + args.app.get('/p/:pad', (req:any, res:any, next:Function) => { // The below might break for pads being rewritten const isReadOnly = !webaccess.userCanModify(req.params.pad, req); @@ -99,7 +99,7 @@ exports.expressCreateServer = (hookName, args, cb) => { }); // serve timeslider.html under /p/$padname/timeslider - args.app.get('/p/:pad/timeslider', (req, res, next) => { + args.app.get('/p/:pad/timeslider', (req:any, res:any, next:Function) => { hooks.callAll('padInitToolbar', { toolbar, }); @@ -112,7 +112,7 @@ exports.expressCreateServer = (hookName, args, cb) => { // The client occasionally polls this endpoint to get an updated expiration for the express_sid // cookie. This handler must be installed after the express-session middleware. - args.app.put('/_extendExpressSessionLifetime', (req, res) => { + args.app.put('/_extendExpressSessionLifetime', (req:any, res:any) => { // express-session automatically calls req.session.touch() so we don't need to do it here. res.json({status: 'ok'}); }); diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.ts similarity index 80% rename from src/node/hooks/express/static.js rename to src/node/hooks/express/static.ts index 26c18995a..6b0b75593 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.ts @@ -1,5 +1,8 @@ 'use strict'; +import {MapArrayType} from "../../types/MapType"; +import {PartType} from "../../types/PartType"; + const fs = require('fs').promises; const minify = require('../../utils/Minify'); const path = require('path'); @@ -10,16 +13,17 @@ const Yajsml = require('etherpad-yajsml'); // Rewrite tar to include modules with no extensions and proper rooted paths. const getTar = async () => { - const prefixLocalLibraryPath = (path) => { + const prefixLocalLibraryPath = (path:string) => { if (path.charAt(0) === '$') { return path.slice(1); } else { return `ep_etherpad-lite/static/js/${path}`; } }; + const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8'); - const tar = {}; - for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) { + const tar:MapArrayType = {}; + for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson)) as [string, string[]][]) { const files = relativeFiles.map(prefixLocalLibraryPath); tar[prefixLocalLibraryPath(key)] = files .concat(files.map((p) => p.replace(/\.js$/, ''))) @@ -28,7 +32,7 @@ const getTar = async () => { return tar; }; -exports.expressPreSession = async (hookName, {app}) => { +exports.expressPreSession = async (hookName:string, {app}:any) => { // Cache both minified and static. const assetCache = new CachingMiddleware(); app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache)); @@ -58,11 +62,13 @@ exports.expressPreSession = async (hookName, {app}) => { // serve plugin definitions // not very static, but served here so that client can do // require("pluginfw/static/js/plugin-definitions.js"); - app.get('/pluginfw/plugin-definitions.json', (req, res, next) => { - const clientParts = plugins.parts.filter((part) => part.client_hooks != null); - const clientPlugins = {}; - for (const name of new Set(clientParts.map((part) => part.plugin))) { + app.get('/pluginfw/plugin-definitions.json', (req: any, res:any, next:Function) => { + const clientParts = plugins.parts.filter((part: PartType) => part.client_hooks != null); + const clientPlugins:MapArrayType = {}; + for (const name of new Set(clientParts.map((part: PartType) => part.plugin))) { + // @ts-ignore clientPlugins[name] = {...plugins.plugins[name]}; + // @ts-ignore delete clientPlugins[name].package; } res.setHeader('Content-Type', 'application/json; charset=utf-8'); diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.ts similarity index 80% rename from src/node/hooks/express/tests.js rename to src/node/hooks/express/tests.ts index 66b47d2af..f8a1417ef 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.ts @@ -1,5 +1,8 @@ 'use strict'; +import {Dirent} from "node:fs"; +import {PluginDef} from "../../types/PartType"; + const path = require('path'); const fsp = require('fs').promises; const plugins = require('../../../static/js/pluginfw/plugin_defs'); @@ -8,15 +11,15 @@ const settings = require('../../utils/Settings'); // Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/' // instead of path.sep to separate pathname components. -const findSpecs = async (specDir) => { - let dirents; +const findSpecs = async (specDir: string) => { + let dirents: Dirent[]; try { dirents = await fsp.readdir(specDir, {withFileTypes: true}); - } catch (err) { + } catch (err:any) { if (['ENOENT', 'ENOTDIR'].includes(err.code)) return []; throw err; } - const specs = []; + const specs: string[] = []; await Promise.all(dirents.map(async (dirent) => { if (dirent.isDirectory()) { const subdirSpecs = await findSpecs(path.join(specDir, dirent.name)); @@ -29,12 +32,12 @@ const findSpecs = async (specDir) => { return specs; }; -exports.expressPreSession = async (hookName, {app}) => { - app.get('/tests/frontend/frontendTestSpecs.json', (req, res, next) => { +exports.expressPreSession = async (hookName:string, {app}:any) => { + app.get('/tests/frontend/frontendTestSpecs.json', (req:any, res:any, next:Function) => { (async () => { - const modules = []; + const modules:string[] = []; await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => { - let {package: {path: pluginPath}} = def; + let {package: {path: pluginPath}} = def as PluginDef; if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep; const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`; for (const spec of await findSpecs(path.join(pluginPath, specDir))) { @@ -59,14 +62,14 @@ exports.expressPreSession = async (hookName, {app}) => { const rootTestFolder = path.join(settings.root, 'src/tests/frontend/'); - app.get('/tests/frontend/index.html', (req, res) => { + app.get('/tests/frontend/index.html', (req:any, res:any) => { res.redirect(['./', ...req.url.split('?').slice(1)].join('?')); }); // The regexp /[\d\D]{0,}/ is equivalent to the regexp /.*/. The Express route path used here // uses the more verbose /[\d\D]{0,}/ pattern instead of /.*/ because path-to-regexp v0.1.7 (the // version used with Express v4.x) interprets '.' and '*' differently than regexp. - app.get('/tests/frontend/:file([\\d\\D]{0,})', (req, res, next) => { + app.get('/tests/frontend/:file([\\d\\D]{0,})', (req:any, res:any, next:Function) => { (async () => { let file = sanitizePathname(req.params.file); if (['', '.', './'].includes(file)) file = 'index.html'; @@ -74,7 +77,7 @@ exports.expressPreSession = async (hookName, {app}) => { })().catch((err) => next(err || new Error(err))); }); - app.get('/tests/frontend', (req, res) => { + app.get('/tests/frontend', (req:any, res:any) => { res.redirect(['./frontend/', ...req.url.split('?').slice(1)].join('?')); }); }; diff --git a/src/node/types/PadType.ts b/src/node/types/PadType.ts index eab10d905..66a9d6ab1 100644 --- a/src/node/types/PadType.ts +++ b/src/node/types/PadType.ts @@ -1,16 +1,44 @@ +import {MapArrayType} from "./MapType"; + export type PadType = { + id: string, apool: ()=>APool, atext: AText, - getInternalRevisionAText: (text:string)=>Promise + pool: APool, + getInternalRevisionAText: (text:string)=>Promise, + getValidRevisionRange: (fromRev: string, toRev: string)=>PadRange, + getRevision: (rev?: string)=>Promise, + head: number, + getAllAuthorColors: ()=>Promise>, } -type APool = { - putAttrib: ([],flag: boolean)=>number +type PadRange = { + startRev: string, + endRev: string, +} + + +export type APool = { + putAttrib: ([],flag?: boolean)=>number, + numToAttrib: MapArrayType, + toJsonable: ()=>any, + clone: ()=>APool, + check: ()=>Promise, + eachAttrib: (callback: (key: string, value: any)=>void)=>void, } export type AText = { text: string, attribs: any -} \ No newline at end of file +} + + +export type PadAuthor = { + +} + +export type AChangeSet = { + +} diff --git a/src/node/types/PartType.ts b/src/node/types/PartType.ts new file mode 100644 index 000000000..3785b73e8 --- /dev/null +++ b/src/node/types/PartType.ts @@ -0,0 +1,10 @@ +export type PartType = { + plugin: string, + client_hooks:any +} + +export type PluginDef = { + package:{ + path:string + } +} diff --git a/src/node/types/UserSettingsObject.ts b/src/node/types/UserSettingsObject.ts new file mode 100644 index 000000000..7cd196866 --- /dev/null +++ b/src/node/types/UserSettingsObject.ts @@ -0,0 +1,5 @@ +export type UserSettingsObject = { + canCreate: boolean, + readOnly: boolean, + padAuthorizations: any +} diff --git a/src/node/utils/Cli.js b/src/node/utils/Cli.ts similarity index 100% rename from src/node/utils/Cli.js rename to src/node/utils/Cli.ts diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.ts similarity index 92% rename from src/node/utils/ExportHtml.js rename to src/node/utils/ExportHtml.ts index d14f40e6e..3b84c4380 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.ts @@ -1,4 +1,7 @@ 'use strict'; +import {AText, PadType} from "../types/PadType"; +import {MapArrayType} from "../types/MapType"; + /** * Copyright 2009 Google Inc. * @@ -26,7 +29,7 @@ const _analyzeLine = require('./ExportHelper')._analyzeLine; const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; const padutils = require('../../static/js/pad_utils').padutils; -const getPadHTML = async (pad, revNum) => { +const getPadHTML = async (pad: PadType, revNum: string) => { let atext = pad.atext; // fetch revision atext @@ -38,7 +41,7 @@ const getPadHTML = async (pad, revNum) => { return await getHTMLFromAtext(pad, atext); }; -const getHTMLFromAtext = async (pad, atext, authorColors) => { +const getHTMLFromAtext = async (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); @@ -48,7 +51,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { await Promise.all([ // prepare tags stored as ['tag', true] to be exported - hooks.aCallAll('exportHtmlAdditionalTags', pad).then((newProps) => { + hooks.aCallAll('exportHtmlAdditionalTags', pad).then((newProps: string[]) => { newProps.forEach((prop) => { tags.push(prop); props.push(prop); @@ -56,7 +59,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { }), // prepare tags stored as ['tag', 'value'] to be exported. This will generate HTML with tags // like - hooks.aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps) => { + hooks.aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps: string[]) => { newProps.forEach((prop) => { tags.push(`span data-${prop[0]}="${prop[1]}"`); props.push(prop); @@ -68,10 +71,10 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { // and maps them to an index in props // *3:2 -> the attribute *3 means strong // *2:5 -> the attribute *2 means s(trikethrough) - const anumMap = {}; + const anumMap:MapArrayType = {}; let css = ''; - const stripDotFromAuthorID = (id) => id.replace(/\./g, '_'); + const stripDotFromAuthorID = (id: string) => id.replace(/\./g, '_'); if (authorColors) { css += '