Moved more classes to ts. (#6179)

This commit is contained in:
SamTV12345 2024-02-22 11:36:43 +01:00 committed by GitHub
parent 3ea6f1072d
commit 4bd27a1c79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 790 additions and 653 deletions

View file

@ -104,7 +104,7 @@ Example returns:
} }
*/ */
exports.getAttributePool = async (padID) => { exports.getAttributePool = async (padID: string) => {
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
return {pool: pad.pool}; 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 // try to parse the revision number
if (rev !== undefined) { if (rev !== undefined) {
rev = checkValidRev(rev); rev = checkValidRev(rev);
@ -155,7 +155,7 @@ Example returns:
{code: 0, message:"ok", data: {text:"Welcome Text"}} {code: 0, message:"ok", data: {text:"Welcome Text"}}
{code: 1, message:"padID does not exist", data: null} {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 // try to parse the revision number
if (rev !== undefined) { if (rev !== undefined) {
rev = checkValidRev(rev); rev = checkValidRev(rev);
@ -173,7 +173,7 @@ exports.getText = async (padID, rev) => {
} }
// get the text of this revision // 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 // Details at https://github.com/ether/etherpad-lite/issues/5073
const {text} = await pad.getInternalRevisionAText(rev); const {text} = await pad.getInternalRevisionAText(rev);
return {text}; return {text};
@ -200,7 +200,7 @@ Example returns:
* @param {String} authorId the id of the author, defaulting to empty string * @param {String} authorId the id of the author, defaulting to empty string
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
exports.setText = async (padID, text, authorId = '') => { exports.setText = async (padID: string, text?: string, authorId: string = ''): Promise<void> => {
// text is required // text is required
if (typeof text !== 'string') { if (typeof text !== 'string') {
throw new CustomError('text is not a string', 'apierror'); 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} text the text of the pad
@param {String} authorId the id of the author, defaulting to empty string @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 // text is required
if (typeof text !== 'string') { if (typeof text !== 'string') {
throw new CustomError('text is not a string', 'apierror'); 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 @param {String} rev the revision number, defaulting to the latest revision
@return {Promise<{html: string}>} the html of the pad @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) { if (rev !== undefined) {
rev = checkValidRev(rev); rev = checkValidRev(rev);
} }
@ -283,7 +283,7 @@ Example returns:
@param {String} html the html of the pad @param {String} html the html of the pad
@param {String} authorId the id of the author, defaulting to empty string @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 // html string is required
if (typeof html !== 'string') { if (typeof html !== 'string') {
throw new CustomError('html is not a string', 'apierror'); 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} start the start point of the chat-history
@param {Number} end the end 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 && end) {
if (start < 0) { if (start < 0) {
throw new CustomError('start is below zero', 'apierror'); throw new CustomError('start is below zero', 'apierror');
@ -374,7 +374,7 @@ Example returns:
@param {String} authorID the id of the author @param {String} authorID the id of the author
@param {Number} time the timestamp of the chat-message @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 // text is required
if (typeof text !== 'string') { if (typeof text !== 'string') {
throw new CustomError('text is not a string', 'apierror'); throw new CustomError('text is not a string', 'apierror');
@ -404,7 +404,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
@param {String} padID the id of the pad @param {String} padID the id of the pad
*/ */
exports.getRevisionsCount = async (padID) => { exports.getRevisionsCount = async (padID: string) => {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
return {revisions: pad.getHeadRevisionNumber()}; return {revisions: pad.getHeadRevisionNumber()};
@ -419,7 +419,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
@param {String} padID the id of the pad @param {String} padID the id of the pad
*/ */
exports.getSavedRevisionsCount = async (padID) => { exports.getSavedRevisionsCount = async (padID: string) => {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
return {savedRevisions: pad.getSavedRevisionsNumber()}; return {savedRevisions: pad.getSavedRevisionsNumber()};
@ -434,7 +434,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
@param {String} padID the id of the pad @param {String} padID the id of the pad
*/ */
exports.listSavedRevisions = async (padID) => { exports.listSavedRevisions = async (padID: string) => {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
return {savedRevisions: pad.getSavedRevisionsList()}; return {savedRevisions: pad.getSavedRevisionsList()};
@ -450,7 +450,7 @@ Example returns:
@param {String} padID the id of the pad @param {String} padID the id of the pad
@param {Number} rev the revision number, defaulting to the latest revision @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 // check if rev is a number
if (rev !== undefined) { if (rev !== undefined) {
rev = checkValidRev(rev); rev = checkValidRev(rev);
@ -483,7 +483,7 @@ Example returns:
@param {String} padID the id of the pad @param {String} padID the id of the pad
@return {Promise<{lastEdited: number}>} the timestamp of the last revision 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 // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
const lastEdited = await pad.getLastEdit(); const lastEdited = await pad.getLastEdit();
@ -497,11 +497,11 @@ Example returns:
{code: 0, message:"ok", data: null} {code: 0, message:"ok", data: null}
{code: 1, message:"pad does already exist", 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} text the initial text of the pad
@param {String} authorId the id of the author, defaulting to empty string @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) { if (padID) {
// ensure there is no $ in the padID // ensure there is no $ in the padID
if (padID.indexOf('$') !== -1) { if (padID.indexOf('$') !== -1) {
@ -527,7 +527,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
@param {String} padID the id of the pad @param {String} padID the id of the pad
*/ */
exports.deletePad = async (padID) => { exports.deletePad = async (padID: string) => {
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
await pad.remove(); await pad.remove();
}; };
@ -543,7 +543,7 @@ exports.deletePad = async (padID) => {
@param {Number} rev the revision number, defaulting to the latest revision @param {Number} rev the revision number, defaulting to the latest revision
@param {String} authorId the id of the author, defaulting to empty string @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 // check if rev is a number
if (rev === undefined) { if (rev === undefined) {
throw new CustomError('rev is not defined', 'apierror'); throw new CustomError('rev is not defined', 'apierror');
@ -563,7 +563,7 @@ exports.restoreRevision = async (padID, rev, authorId = '') => {
const oldText = pad.text(); const oldText = pad.text();
atext.text += '\n'; atext.text += '\n';
const eachAttribRun = (attribs, func) => { const eachAttribRun = (attribs: string[], func:Function) => {
let textIndex = 0; let textIndex = 0;
const newTextStart = 0; const newTextStart = 0;
const newTextEnd = atext.text.length; const newTextEnd = atext.text.length;
@ -580,7 +580,7 @@ exports.restoreRevision = async (padID, rev, authorId = '') => {
const builder = Changeset.builder(oldText.length); const builder = Changeset.builder(oldText.length);
// assemble each line into the builder // 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); builder.insert(atext.text.substring(start, end), attribs);
}); });
@ -610,7 +610,7 @@ Example returns:
@param {String} destinationID the id of the destination pad @param {String} destinationID the id of the destination pad
@param {Boolean} force whether to overwrite the destination pad if it exists @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); const pad = await getPadSafe(sourceID, true);
await pad.copy(destinationID, force); await pad.copy(destinationID, force);
}; };
@ -628,7 +628,7 @@ Example returns:
@param {Boolean} force whether to overwrite the destination pad if it exists @param {Boolean} force whether to overwrite the destination pad if it exists
@param {String} authorId the id of the author, defaulting to empty string @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); const pad = await getPadSafe(sourceID, true);
await pad.copyPadWithoutHistory(destinationID, force, authorId); await pad.copyPadWithoutHistory(destinationID, force, authorId);
}; };
@ -645,7 +645,7 @@ Example returns:
@param {String} destinationID the id of the destination pad @param {String} destinationID the id of the destination pad
@param {Boolean} force whether to overwrite the destination pad if it exists @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); const pad = await getPadSafe(sourceID, true);
await pad.copy(destinationID, force); await pad.copy(destinationID, force);
await pad.remove(); await pad.remove();
@ -660,7 +660,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
@param {String} padID the id of the pad @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 // we don't need the pad object, but this function does all the security stuff for us
await getPadSafe(padID, true); await getPadSafe(padID, true);
@ -679,7 +679,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
@param {String} roID the readonly id of the pad @param {String} roID the readonly id of the pad
*/ */
exports.getPadID = async (roID) => { exports.getPadID = async (roID: string) => {
// get the PadId // get the PadId
const padID = await readOnlyManager.getPadId(roID); const padID = await readOnlyManager.getPadId(roID);
if (padID == null) { if (padID == null) {
@ -699,7 +699,7 @@ Example returns:
@param {String} padID the id of the pad @param {String} padID the id of the pad
@param {Boolean} publicStatus the public status 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 // ensure this is a group pad
checkGroupPad(padID, 'publicStatus'); checkGroupPad(padID, 'publicStatus');
@ -723,7 +723,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
@param {String} padID the id of the pad @param {String} padID the id of the pad
*/ */
exports.getPublicStatus = async (padID) => { exports.getPublicStatus = async (padID: string) => {
// ensure this is a group pad // ensure this is a group pad
checkGroupPad(padID, 'publicStatus'); checkGroupPad(padID, 'publicStatus');
@ -741,7 +741,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
@param {String} padID the id of the pad @param {String} padID the id of the pad
*/ */
exports.listAuthorsOfPad = async (padID) => { exports.listAuthorsOfPad = async (padID: string) => {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
const authorIDs = pad.getAllAuthors(); const authorIDs = pad.getAllAuthors();
@ -773,7 +773,7 @@ Example returns:
@param {String} msg the message to send @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. await getPadSafe(padID, true); // Throw if the padID is invalid or if the pad does not exist.
padMessageHandler.handleCustomMessage(padID, msg); padMessageHandler.handleCustomMessage(padID, msg);
}; };
@ -799,7 +799,7 @@ Example returns:
@param {String} padID the id of the pad @param {String} padID the id of the pad
@return {Promise<{chatHead: number}>} the chatHead 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 // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
return {chatHead: pad.chatHead}; return {chatHead: pad.chatHead};
@ -825,7 +825,7 @@ Example returns:
@param {Number} startRev the start revision number @param {Number} startRev the start revision number
@param {Number} endRev the end 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 // check if startRev is a number
if (startRev !== undefined) { if (startRev !== undefined) {
startRev = checkValidRev(startRev); startRev = checkValidRev(startRev);
@ -846,7 +846,7 @@ exports.createDiffHTML = async (padID, startRev, endRev) => {
let padDiff; let padDiff;
try { try {
padDiff = new PadDiff(pad, startRev, endRev); padDiff = new PadDiff(pad, startRev, endRev);
} catch (e) { } catch (e:any) {
throw {stop: e.message}; throw {stop: e.message};
} }
@ -872,6 +872,7 @@ exports.getStats = async () => {
const sessionInfos = padMessageHandler.sessioninfos; const sessionInfos = padMessageHandler.sessioninfos;
const sessionKeys = Object.keys(sessionInfos); const sessionKeys = Object.keys(sessionInfos);
// @ts-ignore
const activePads = new Set(Object.entries(sessionInfos).map((k) => k[1].padId)); const activePads = new Set(Object.entries(sessionInfos).map((k) => k[1].padId));
const {padIDs} = await padManager.listAllPads(); const {padIDs} = await padManager.listAllPads();
@ -888,7 +889,7 @@ exports.getStats = async () => {
**************************** */ **************************** */
// gets a pad safe // 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 // check if padID is a string
if (typeof padID !== 'string') { if (typeof padID !== 'string') {
throw new CustomError('padID is not a string', 'apierror'); 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 // 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 // ensure this is a group pad
if (padID && padID.indexOf('$') === -1) { if (padID && padID.indexOf('$') === -1) {
throw new CustomError( throw new CustomError(

View file

@ -95,7 +95,7 @@ exports.getColorPalette = () => [
* Checks if the author exists * Checks if the author exists
* @param {String} authorID The id of the author * @param {String} authorID The id of the author
*/ */
exports.doesAuthorExist = async (authorID) => { exports.doesAuthorExist = async (authorID: string) => {
const author = await db.get(`globalAuthor:${authorID}`); const author = await db.get(`globalAuthor:${authorID}`);
return author != null; return author != null;
@ -114,7 +114,7 @@ exports.doesAuthorExists = exports.doesAuthorExist;
* @param {String} mapperkey The database key name for this mapper * @param {String} mapperkey The database key name for this mapper
* @param {String} mapper The mapper * @param {String} mapper The mapper
*/ */
const mapAuthorWithDBKey = async (mapperkey, mapper) => { const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => {
// try to map to an author // try to map to an author
const author = await db.get(`${mapperkey}:${mapper}`); const author = await db.get(`${mapperkey}:${mapper}`);
@ -142,7 +142,7 @@ const mapAuthorWithDBKey = async (mapperkey, mapper) => {
* @param {String} token The token of the author * @param {String} token The token of the author
* @return {Promise<string|*|{authorID: string}|{authorID: *}>} * @return {Promise<string|*|{authorID: string}|{authorID: *}>}
*/ */
const getAuthor4Token = async (token) => { const getAuthor4Token = async (token: string) => {
const author = await mapAuthorWithDBKey('token2author', token); const author = await mapAuthorWithDBKey('token2author', token);
// return only the sub value authorID // return only the sub value authorID
@ -155,7 +155,7 @@ const getAuthor4Token = async (token) => {
* @param {Object} user * @param {Object} user
* @return {Promise<*>} * @return {Promise<*>}
*/ */
exports.getAuthorId = async (token, user) => { exports.getAuthorId = async (token: string, user: object) => {
const context = {dbKey: token, token, user}; const context = {dbKey: token, token, user};
let [authorId] = await hooks.aCallFirst('getAuthorId', context); let [authorId] = await hooks.aCallFirst('getAuthorId', context);
if (!authorId) authorId = await getAuthor4Token(context.dbKey); if (!authorId) authorId = await getAuthor4Token(context.dbKey);
@ -168,7 +168,7 @@ exports.getAuthorId = async (token, user) => {
* @deprecated Use `getAuthorId` instead. * @deprecated Use `getAuthorId` instead.
* @param {String} token The token * @param {String} token The token
*/ */
exports.getAuthor4Token = async (token) => { exports.getAuthor4Token = async (token: string) => {
warnDeprecated( warnDeprecated(
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead'); 'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
return await getAuthor4Token(token); return await getAuthor4Token(token);
@ -179,7 +179,7 @@ exports.getAuthor4Token = async (token) => {
* @param {String} authorMapper The mapper * @param {String} authorMapper The mapper
* @param {String} name The name of the author (optional) * @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); const author = await mapAuthorWithDBKey('mapper2author', authorMapper);
if (name) { if (name) {
@ -195,7 +195,7 @@ exports.createAuthorIfNotExistsFor = async (authorMapper, name) => {
* Internal function that creates the database entry for an author * Internal function that creates the database entry for an author
* @param {String} name The name of the author * @param {String} name The name of the author
*/ */
exports.createAuthor = async (name) => { exports.createAuthor = async (name: string) => {
// create the new author name // create the new author name
const author = `a.${randomString(16)}`; const author = `a.${randomString(16)}`;
@ -216,41 +216,41 @@ exports.createAuthor = async (name) => {
* Returns the Author Obj of the author * Returns the Author Obj of the author
* @param {String} author The id 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 * Returns the color Id of the author
* @param {String} author The 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 * Sets the color Id of the author
* @param {String} author The id of the author * @param {String} author The id of the author
* @param {String} colorId The color 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); `globalAuthor:${author}`, ['colorId'], colorId);
/** /**
* Returns the name of the author * Returns the name of the author
* @param {String} author The id 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 * Sets the name of the author
* @param {String} author The id of the author * @param {String} author The id of the author
* @param {String} name The name 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); `globalAuthor:${author}`, ['name'], name);
/** /**
* Returns an array of all pads this author contributed to * Returns an array of all pads this author contributed to
* @param {String} authorID The id of the author * @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: /* 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 * (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 * (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} authorID The id of the author
* @param {String} padID The id of the pad the author contributes to * @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 // get the entry
const author = await db.get(`globalAuthor:${authorID}`); 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} authorID The id of the author
* @param {String} padID The id of the pad the author contributes to * @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}`); const author = await db.get(`globalAuthor:${authorID}`);
if (author == null) return; if (author == null) return;

View file

@ -42,7 +42,7 @@ exports.listAllGroups = async () => {
* @param {String} groupID The id of the group * @param {String} groupID The id of the group
* @return {Promise<void>} Resolves when the group is deleted * @return {Promise<void>} Resolves when the group is deleted
*/ */
exports.deleteGroup = async (groupID) => { exports.deleteGroup = async (groupID: string): Promise<void> => {
const group = await db.get(`group:${groupID}`); const group = await db.get(`group:${groupID}`);
// ensure group exists // ensure group exists
@ -82,7 +82,7 @@ exports.deleteGroup = async (groupID) => {
* @param {String} groupID the id of the group to delete * @param {String} groupID the id of the group to delete
* @return {Promise<boolean>} Resolves to true if the group exists * @return {Promise<boolean>} Resolves to true if the group exists
*/ */
exports.doesGroupExist = async (groupID) => { exports.doesGroupExist = async (groupID: string) => {
// try to get the group entry // try to get the group entry
const group = await db.get(`group:${groupID}`); const group = await db.get(`group:${groupID}`);
@ -108,7 +108,7 @@ exports.createGroup = async () => {
* @param groupMapper the mapper of the group * @param groupMapper the mapper of the group
* @return {Promise<{groupID: string}|{groupID: *}>} a promise that resolves to the group ID * @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') { if (typeof groupMapper !== 'string') {
throw new CustomError('groupMapper is not a string', 'apierror'); 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 * @param {String} authorId The id of the author
* @return {Promise<{padID: string}>} a promise that resolves to the id of the new pad * @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 // create the padID
const padID = `${groupID}$${padName}`; const padID = `${groupID}$${padName}`;
@ -167,7 +167,7 @@ exports.createGroupPad = async (groupID, padName, text, authorId = '') => {
* @param {String} groupID The id of the group * @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 * @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); const exists = await exports.doesGroupExist(groupID);
// ensure the group exists // ensure the group exists

View file

@ -1,4 +1,8 @@
'use strict'; 'use strict';
import {Database} from "ueberdb2";
import {AChangeSet, APool, AText} from "../types/PadType";
import {MapArrayType} from "../types/MapType";
/** /**
* The pad object, defined with joose * The pad object, defined with joose
*/ */
@ -28,20 +32,29 @@ const promises = require('../utils/promises');
* @param {String} txt The text to clean * @param {String} txt The text to clean
* @returns {String} The cleaned text * @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(/\r/g, '\n')
.replace(/\t/g, ' ') .replace(/\t/g, ' ')
.replace(/\xa0/g, ' '); .replace(/\xa0/g, ' ');
class Pad { 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; * @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 * 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 * 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 * 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. * 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.db = database;
this.atext = Changeset.makeAText('\n'); this.atext = Changeset.makeAText('\n');
this.pool = new AttributePool(); this.pool = new AttributePool();
@ -80,7 +93,7 @@ class Pad {
* @param {String} authorId The id of the author * @param {String} authorId The id of the author
* @return {Promise<number|string>} * @return {Promise<number|string>}
*/ */
async appendRevision(aChangeset, authorId = '') { async appendRevision(aChangeset:AChangeSet, authorId = '') {
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool); const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs && if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
this.head !== -1) { this.head !== -1) {
@ -95,6 +108,7 @@ class Pad {
const hook = this.head === 0 ? 'padCreate' : 'padUpdate'; const hook = this.head === 0 ? 'padCreate' : 'padUpdate';
await Promise.all([ await Promise.all([
// @ts-ignore
this.db.set(`pad:${this.id}:revs:${newRev}`, { this.db.set(`pad:${this.id}:revs:${newRev}`, {
changeset: aChangeset, changeset: aChangeset,
meta: { meta: {
@ -129,32 +143,39 @@ class Pad {
} }
toJSON() { toJSON() {
const o = {...this, pool: this.pool.toJsonable()}; const o:Pad = {...this, pool: this.pool.toJsonable()};
// @ts-ignore
delete o.db; delete o.db;
// @ts-ignore
delete o.id; delete o.id;
return o; return o;
} }
// save all attributes to the database // save all attributes to the database
async saveToDatabase() { async saveToDatabase() {
// @ts-ignore
await this.db.set(`pad:${this.id}`, this); await this.db.set(`pad:${this.id}`, this);
} }
// get time of last edit (changeset application) // get time of last edit (changeset application)
async getLastEdit() { async getLastEdit() {
const revNum = this.getHeadRevisionNumber(); const revNum = this.getHeadRevisionNumber();
// @ts-ignore
return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'timestamp']); 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']); 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']); 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']); 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`). * @param {number} revNum - Must be a key revision number (see `getKeyRevisionNumber`).
* @returns The attribute text stored at `revNum`. * @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']); return await this.db.getSub(`pad:${this.id}:revs:${revNum}`, ['meta', 'atext']);
} }
@ -182,7 +204,7 @@ class Pad {
return authorIds; return authorIds;
} }
async getInternalRevisionAText(targetRev) { async getInternalRevisionAText(targetRev: number) {
const keyRev = this.getKeyRevisionNumber(targetRev); const keyRev = this.getKeyRevisionNumber(targetRev);
const headRev = this.getHeadRevisionNumber(); const headRev = this.getHeadRevisionNumber();
if (targetRev > headRev) targetRev = headRev; if (targetRev > headRev) targetRev = headRev;
@ -197,17 +219,17 @@ class Pad {
return atext; return atext;
} }
async getRevision(revNum) { async getRevision(revNum: number) {
return await this.db.get(`pad:${this.id}:revs:${revNum}`); return await this.db.get(`pad:${this.id}:revs:${revNum}`);
} }
async getAllAuthorColors() { async getAllAuthorColors() {
const authorIds = this.getAllAuthors(); const authorIds = this.getAllAuthors();
const returnTable = {}; const returnTable:MapArrayType<string> = {};
const colorPalette = authorManager.getColorPalette(); const colorPalette = authorManager.getColorPalette();
await Promise.all( 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 // colorId might be a hex color or an number out of the palette
returnTable[authorId] = colorPalette[colorId] || colorId; returnTable[authorId] = colorPalette[colorId] || colorId;
}))); })));
@ -215,7 +237,7 @@ class Pad {
return returnTable; return returnTable;
} }
getValidRevisionRange(startRev, endRev) { getValidRevisionRange(startRev: any, endRev:any) {
startRev = parseInt(startRev, 10); startRev = parseInt(startRev, 10);
const head = this.getHeadRevisionNumber(); const head = this.getHeadRevisionNumber();
endRev = endRev ? parseInt(endRev, 10) : head; endRev = endRev ? parseInt(endRev, 10) : head;
@ -236,14 +258,14 @@ class Pad {
return null; return null;
} }
getKeyRevisionNumber(revNum) { getKeyRevisionNumber(revNum: number) {
return Math.floor(revNum / 100) * 100; return Math.floor(revNum / 100) * 100;
} }
/** /**
* @returns {string} The pad's text. * @returns {string} The pad's text.
*/ */
text() { text(): string {
return this.atext.text; 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} 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). * @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 (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})`); if (ndel < 0) throw new RangeError(`characters to delete must be non-negative (is ${ndel})`);
const orig = this.text(); const orig = this.text();
@ -283,7 +305,7 @@ class Pad {
* @param {string} [authorId] - The author ID of the user that initiated the change, if * @param {string} [authorId] - The author ID of the user that initiated the change, if
* applicable. * applicable.
*/ */
async setText(newText, authorId = '') { async setText(newText: string, authorId = '') {
await this.spliceText(0, this.text().length, newText, 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 * @param {string} [authorId] - The author ID of the user that initiated the change, if
* applicable. * applicable.
*/ */
async appendText(newText, authorId = '') { async appendText(newText:string, authorId = '') {
await this.spliceText(this.text().length - 1, 0, newText, 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 * @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
* `msgOrText.time` instead. * `msgOrText.time` instead.
*/ */
async appendChatMessage(msgOrText, authorId = null, time = null) { async appendChatMessage(msgOrText: string|typeof ChatMessage, authorId = null, time = null) {
const msg = const msg =
msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time); msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time);
this.chatHead++; this.chatHead++;
@ -325,7 +347,7 @@ class Pad {
* @param {number} entryNum - ID of the desired chat message. * @param {number} entryNum - ID of the desired chat message.
* @returns {?ChatMessage} * @returns {?ChatMessage}
*/ */
async getChatMessage(entryNum) { async getChatMessage(entryNum: number) {
const entry = await this.db.get(`pad:${this.id}:chat:${entryNum}`); const entry = await this.db.get(`pad:${this.id}:chat:${entryNum}`);
if (entry == null) return null; if (entry == null) return null;
const message = ChatMessage.fromObject(entry); 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 * (inclusive), in order. Note: `start` and `end` form a closed interval, not a half-open
* interval as is typical in code. * interval as is typical in code.
*/ */
async getChatMessages(start, end) { async getChatMessages(start: string, end: number) {
const entries = const entries =
await Promise.all(Stream.range(start, end + 1).map(this.getChatMessage.bind(this))); 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 // try to load the pad
const value = await this.db.get(`pad:${this.id}`); const value = await this.db.get(`pad:${this.id}`);
@ -377,7 +399,7 @@ class Pad {
await hooks.aCallAll('padLoad', {pad: this}); await hooks.aCallAll('padLoad', {pad: this});
} }
async copy(destinationID, force) { async copy(destinationID: string, force: boolean) {
// Kick everyone from this pad. // Kick everyone from this pad.
// This was commented due to https://github.com/ether/etherpad-lite/issues/3183. // This was commented due to https://github.com/ether/etherpad-lite/issues/3183.
// Do we really need to kick everyone out? // 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 // if force is true and already exists a Pad with the same id, remove that Pad
await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force); await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force);
const copyRecord = async (keySuffix) => { const copyRecord = async (keySuffix: string) => {
const val = await this.db.get(`pad:${this.id}${keySuffix}`); const val = await this.db.get(`pad:${this.id}${keySuffix}`);
await db.set(`pad:${destinationID}${keySuffix}`, val); await db.set(`pad:${destinationID}${keySuffix}`, val);
}; };
const promises = (function* () { const promises = (function* () {
yield copyRecord(''); yield copyRecord('');
// @ts-ignore
yield* Stream.range(0, this.head + 1).map((i) => copyRecord(`:revs:${i}`)); 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}`)); yield* Stream.range(0, this.chatHead + 1).map((i) => copyRecord(`:chat:${i}`));
// @ts-ignore
yield this.copyAuthorInfoToDestinationPad(destinationID); yield this.copyAuthorInfoToDestinationPad(destinationID);
if (destGroupID) yield db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1); if (destGroupID) yield db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1);
}).call(this); }).call(this);
@ -427,8 +452,8 @@ class Pad {
return {padID: destinationID}; return {padID: destinationID};
} }
async checkIfGroupExistAndReturnIt(destinationID) { async checkIfGroupExistAndReturnIt(destinationID: string) {
let destGroupID = false; let destGroupID:false|string = false;
if (destinationID.indexOf('$') >= 0) { if (destinationID.indexOf('$') >= 0) {
destGroupID = destinationID.split('$')[0]; destGroupID = destinationID.split('$')[0];
@ -442,7 +467,7 @@ class Pad {
return destGroupID; return destGroupID;
} }
async removePadIfForceIsTrueAndAlreadyExist(destinationID, force) { async removePadIfForceIsTrueAndAlreadyExist(destinationID: string, force: boolean|string) {
// if the pad exists, we should abort, unless forced. // if the pad exists, we should abort, unless forced.
const exists = await padManager.doesPadExist(destinationID); 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 // add the new sourcePad to all authors who contributed to the old one
await Promise.all(this.getAllAuthors().map( await Promise.all(this.getAllAuthors().map(
(authorID) => authorManager.addPad(authorID, destinationID))); (authorID) => authorManager.addPad(authorID, destinationID)));
} }
async copyPadWithoutHistory(destinationID, force, authorId = '') { async copyPadWithoutHistory(destinationID: string, force: string|boolean, authorId = '') {
// flush the source pad // flush the source pad
this.saveToDatabase(); this.saveToDatabase();
@ -554,18 +579,18 @@ class Pad {
} }
// remove the readonly entries // 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}`); await db.remove(`readonly2pad:${readonlyID}`);
})); }));
p.push(db.remove(`pad2readonly:${padID}`)); p.push(db.remove(`pad2readonly:${padID}`));
// delete all chat messages // 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); await this.db.remove(`pad:${this.id}:chat:${i}`, null);
})); }));
// delete all revisions // 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); await this.db.remove(`pad:${this.id}:revs:${i}`, null);
})); }));
@ -587,12 +612,12 @@ class Pad {
} }
// set in db // set in db
async setPublicStatus(publicStatus) { async setPublicStatus(publicStatus: boolean) {
this.publicStatus = publicStatus; this.publicStatus = publicStatus;
await this.saveToDatabase(); await this.saveToDatabase();
} }
async addSavedRevision(revNum, savedById, label) { async addSavedRevision(revNum: string, savedById: string, label: string) {
// if this revision is already saved, return silently // if this revision is already saved, return silently
for (const i in this.savedRevisions) { for (const i in this.savedRevisions) {
if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) { if (this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum) {
@ -601,7 +626,7 @@ class Pad {
} }
// build the saved revision object // build the saved revision object
const savedRevision = {}; const savedRevision:MapArrayType<any> = {};
savedRevision.revNum = revNum; savedRevision.revNum = revNum;
savedRevision.savedById = savedById; savedRevision.savedById = savedById;
savedRevision.label = label || `Revision ${revNum}`; savedRevision.label = label || `Revision ${revNum}`;
@ -664,7 +689,7 @@ class Pad {
if (k === 'author' && v) authorIds.add(v); if (k === 'author' && v) authorIds.add(v);
}); });
const revs = Stream.range(0, head + 1) const revs = Stream.range(0, head + 1)
.map(async (r) => { .map(async (r: number) => {
const isKeyRev = r === this.getKeyRevisionNumber(r); const isKeyRev = r === this.getKeyRevisionNumber(r);
try { try {
return await Promise.all([ return await Promise.all([
@ -675,7 +700,7 @@ class Pad {
isKeyRev, isKeyRev,
isKeyRev ? this._getKeyRevisionAText(r) : null, isKeyRev ? this._getKeyRevisionAText(r) : null,
]); ]);
} catch (err) { } catch (err:any) {
err.message = `(pad ${this.id} revision ${r}) ${err.message}`; err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
throw err; throw err;
} }
@ -708,7 +733,7 @@ class Pad {
} }
atext = Changeset.applyToAText(changeset, atext, pool); atext = Changeset.applyToAText(changeset, atext, pool);
if (isKeyRev) assert.deepEqual(keyAText, atext); if (isKeyRev) assert.deepEqual(keyAText, atext);
} catch (err) { } catch (err:any) {
err.message = `(pad ${this.id} revision ${r}) ${err.message}`; err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
throw err; throw err;
} }
@ -721,12 +746,12 @@ class Pad {
assert(Number.isInteger(this.chatHead)); assert(Number.isInteger(this.chatHead));
assert(this.chatHead >= -1); assert(this.chatHead >= -1);
const chats = Stream.range(0, this.chatHead + 1) const chats = Stream.range(0, this.chatHead + 1)
.map(async (c) => { .map(async (c: number) => {
try { try {
const msg = await this.getChatMessage(c); const msg = await this.getChatMessage(c);
assert(msg != null); assert(msg != null);
assert(msg instanceof ChatMessage); assert(msg instanceof ChatMessage);
} catch (err) { } catch (err:any) {
err.message = `(pad ${this.id} chat message ${c}) ${err.message}`; err.message = `(pad ${this.id} chat message ${c}) ${err.message}`;
throw err; throw err;
} }

View file

@ -19,6 +19,8 @@
* limitations under the License. * limitations under the License.
*/ */
import {MapArrayType} from "../types/MapType";
const CustomError = require('../utils/customError'); const CustomError = require('../utils/customError');
const Pad = require('../db/Pad'); const Pad = require('../db/Pad');
const db = require('./DB'); 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 * If this is needed in other places, it would be wise to make this a prototype
* that's defined somewhere more sensible. * that's defined somewhere more sensible.
*/ */
const globalPads = { const globalPads:MapArrayType<any> = {
get(name) { return this[`:${name}`]; }, get(name: string)
set(name, value) { {
return this[`:${name}`];
},
set(name: string, value: any)
{
this[`:${name}`] = value; this[`:${name}`] = value;
}, },
remove(name) { remove(name: string) {
delete this[`:${name}`]; delete this[`:${name}`];
}, },
}; };
@ -51,6 +57,9 @@ const globalPads = {
* Updated without db access as new pads are created/old ones removed. * Updated without db access as new pads are created/old ones removed.
*/ */
const padList = new class { const padList = new class {
private _cachedList: string[] | null;
private _list: Set<string>;
private _loaded: Promise<void> | null;
constructor() { constructor() {
this._cachedList = null; this._cachedList = null;
this._list = new Set(); this._list = new Set();
@ -74,13 +83,13 @@ const padList = new class {
return this._cachedList; return this._cachedList;
} }
addPad(name) { addPad(name: string) {
if (this._list.has(name)) return; if (this._list.has(name)) return;
this._list.add(name); this._list.add(name);
this._cachedList = null; this._cachedList = null;
} }
removePad(name) { removePad(name: string) {
if (!this._list.has(name)) return; if (!this._list.has(name)) return;
this._list.delete(name); this._list.delete(name);
this._cachedList = null; 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 * @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if
* applicable). * applicable).
*/ */
exports.getPad = async (id, text, authorId = '') => { exports.getPad = async (id: string, text: string, authorId:string = '') => {
// check if this is a valid padId // check if this is a valid padId
if (!exports.isValidPadId(id)) { if (!exports.isValidPadId(id)) {
throw new CustomError(`${id} is not a valid padId`, 'apierror'); throw new CustomError(`${id} is not a valid padId`, 'apierror');
@ -140,7 +149,7 @@ exports.listAllPads = async () => {
}; };
// checks if a pad exists // checks if a pad exists
exports.doesPadExist = async (padId) => { exports.doesPadExist = async (padId: string) => {
const value = await db.get(`pad:${padId}`); const value = await db.get(`pad:${padId}`);
return (value != null && value.atext); return (value != null && value.atext);
@ -159,7 +168,7 @@ const padIdTransforms = [
]; ];
// returns a sanitized padId, respecting legacy pad id formats // 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) { for (let i = 0, n = padIdTransforms.length; i < n; ++i) {
const exists = await exports.doesPadExist(padId); const exists = await exports.doesPadExist(padId);
@ -169,6 +178,7 @@ exports.sanitizePadId = async (padId) => {
const [from, to] = padIdTransforms[i]; const [from, to] = padIdTransforms[i];
// @ts-ignore
padId = padId.replace(from, to); padId = padId.replace(from, to);
} }
@ -178,12 +188,12 @@ exports.sanitizePadId = async (padId) => {
return 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. * Removes the pad from database and unloads it.
*/ */
exports.removePad = async (padId) => { exports.removePad = async (padId: string) => {
const p = db.remove(`pad:${padId}`); const p = db.remove(`pad:${padId}`);
exports.unloadPad(padId); exports.unloadPad(padId);
padList.removePad(padId); padList.removePad(padId);
@ -191,6 +201,6 @@ exports.removePad = async (padId) => {
}; };
// removes a pad from the cache // removes a pad from the cache
exports.unloadPad = (padId) => { exports.unloadPad = (padId: string) => {
globalPads.remove(padId); globalPads.remove(padId);
}; };

View file

@ -29,14 +29,14 @@ const randomString = require('../utils/randomstring');
* @param {String} id the pad's id * @param {String} id the pad's id
* @return {Boolean} true if the id is readonly * @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 * returns a read only id for a pad
* @param {String} padId the id of the pad * @param {String} padId the id of the pad
* @return {String} the read only id * @return {String} the read only id
*/ */
exports.getReadOnlyId = async (padId) => { exports.getReadOnlyId = async (padId:string) => {
// check if there is a pad2readonly entry // check if there is a pad2readonly entry
let readOnlyId = await db.get(`pad2readonly:${padId}`); let readOnlyId = await db.get(`pad2readonly:${padId}`);
@ -57,14 +57,14 @@ exports.getReadOnlyId = async (padId) => {
* @param {String} readOnlyId read only id * @param {String} readOnlyId read only id
* @return {String} the padId * @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 * returns the padId and readonlyPadId in an object for any id
* @param {String} id read only id or real pad id * @param {String} id read only id or real pad id
* @return {Object} an object with the padId and readonlyPadId * @return {Object} an object with the padId and readonlyPadId
*/ */
exports.getIds = async (id) => { exports.getIds = async (id:string) => {
const readonly = exports.isReadOnlyId(id); const readonly = exports.isReadOnlyId(id);
// Might be null, if this is an unknown read-only id // Might be null, if this is an unknown read-only id

View file

@ -19,6 +19,8 @@
* limitations under the License. * limitations under the License.
*/ */
import {UserSettingsObject} from "../types/UserSettingsObject";
const authorManager = require('./AuthorManager'); const authorManager = require('./AuthorManager');
const hooks = require('../../static/js/pluginfw/hooks.js'); const hooks = require('../../static/js/pluginfw/hooks.js');
const padManager = require('./PadManager'); const padManager = require('./PadManager');
@ -55,7 +57,7 @@ const DENY = Object.freeze({accessStatus: 'deny'});
* @param {Object} userSettings * @param {Object} userSettings
* @return {DENY|{accessStatus: String, authorID: String}} * @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) { if (!padID) {
authLogger.debug('access denied: missing padID'); authLogger.debug('access denied: missing padID');
return DENY; return DENY;
@ -95,7 +97,7 @@ exports.checkAccess = async (padID, sessionCookie, token, userSettings) => {
} }
// allow plugins to deny access // 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)) { if (hooks.callAll('onAccessCheck', {padID, token, sessionCookie}).some(isFalse)) {
authLogger.debug('access denied: an onAccessCheck hook function returned false'); authLogger.debug('access denied: an onAccessCheck hook function returned false');
return DENY; return DENY;

View file

@ -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 * sessionCookie, and is bound to a group with the given ID, then this returns the author ID
* bound to the session. Otherwise, returns undefined. * bound to the session. Otherwise, returns undefined.
*/ */
exports.findAuthorID = async (groupID, sessionCookie) => { exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
if (!sessionCookie) return undefined; if (!sessionCookie) return undefined;
/* /*
* Sometimes, RFC 6265-compliant web servers may send back a cookie whose * 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) => { const sessionInfoPromises = sessionIDs.map(async (id) => {
try { try {
return await exports.getSessionInfo(id); return await exports.getSessionInfo(id);
} catch (err) { } catch (err:any) {
if (err.message === 'sessionID does not exist') { if (err.message === 'sessionID does not exist') {
console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`); console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`);
} else { } else {
@ -75,7 +75,10 @@ exports.findAuthorID = async (groupID, sessionCookie) => {
return undefined; return undefined;
}); });
const now = Math.floor(Date.now() / 1000); 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); const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch);
if (sessionInfo == null) return undefined; if (sessionInfo == null) return undefined;
return sessionInfo.authorID; return sessionInfo.authorID;
@ -86,7 +89,7 @@ exports.findAuthorID = async (groupID, sessionCookie) => {
* @param {String} sessionID The id of the session * @param {String} sessionID The id of the session
* @return {Promise<boolean>} Resolves to true if the session exists * @return {Promise<boolean>} 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 // check if the database entry of this session exists
const session = await db.get(`session:${sessionID}`); const session = await db.get(`session:${sessionID}`);
return (session != null); return (session != null);
@ -99,7 +102,7 @@ exports.doesSessionExist = async (sessionID) => {
* @param {Number} validUntil The unix timestamp when the session should expire * @param {Number} validUntil The unix timestamp when the session should expire
* @return {Promise<{sessionID: string}>} the id of the new session * @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 // check if the group exists
const groupExists = await groupManager.doesGroupExist(groupID); const groupExists = await groupManager.doesGroupExist(groupID);
if (!groupExists) { if (!groupExists) {
@ -160,7 +163,7 @@ exports.createSession = async (groupID, authorID, validUntil) => {
* @param {String} sessionID The id of the session * @param {String} sessionID The id of the session
* @return {Promise<Object>} the sessioninfos * @return {Promise<Object>} the sessioninfos
*/ */
exports.getSessionInfo = async (sessionID) => { exports.getSessionInfo = async (sessionID:string) => {
// check if the database entry of this session exists // check if the database entry of this session exists
const session = await db.get(`session:${sessionID}`); const session = await db.get(`session:${sessionID}`);
@ -178,7 +181,7 @@ exports.getSessionInfo = async (sessionID) => {
* @param {String} sessionID The id of the session * @param {String} sessionID The id of the session
* @return {Promise<void>} Resolves when the session is deleted * @return {Promise<void>} Resolves when the session is deleted
*/ */
exports.deleteSession = async (sessionID) => { exports.deleteSession = async (sessionID:string) => {
// ensure that the session exists // ensure that the session exists
const session = await db.get(`session:${sessionID}`); const session = await db.get(`session:${sessionID}`);
if (session == null) { if (session == null) {
@ -207,7 +210,7 @@ exports.deleteSession = async (sessionID) => {
* @param {String} groupID The id of the group * @param {String} groupID The id of the group
* @return {Promise<Object>} The sessioninfos of all sessions of this group * @return {Promise<Object>} The sessioninfos of all sessions of this group
*/ */
exports.listSessionsOfGroup = async (groupID) => { exports.listSessionsOfGroup = async (groupID: string) => {
// check that the group exists // check that the group exists
const exists = await groupManager.doesGroupExist(groupID); const exists = await groupManager.doesGroupExist(groupID);
if (!exists) { if (!exists) {
@ -223,7 +226,7 @@ exports.listSessionsOfGroup = async (groupID) => {
* @param {String} authorID The id of the author * @param {String} authorID The id of the author
* @return {Promise<Object>} The sessioninfos of all sessions of this author * @return {Promise<Object>} The sessioninfos of all sessions of this author
*/ */
exports.listSessionsOfAuthor = async (authorID) => { exports.listSessionsOfAuthor = async (authorID: string) => {
// check that the author exists // check that the author exists
const exists = await authorManager.doesAuthorExist(authorID); const exists = await authorManager.doesAuthorExist(authorID);
if (!exists) { if (!exists) {
@ -240,7 +243,7 @@ exports.listSessionsOfAuthor = async (authorID) => {
* @param {String} dbkey The db key to use to get the sessions * @param {String} dbkey The db key to use to get the sessions
* @return {Promise<*>} * @return {Promise<*>}
*/ */
const listSessionsWithDBKey = async (dbkey) => { const listSessionsWithDBKey = async (dbkey: string) => {
// get the group2sessions entry // get the group2sessions entry
const sessionObject = await db.get(dbkey); const sessionObject = await db.get(dbkey);
const sessions = sessionObject ? sessionObject.sessionIDs : null; const sessions = sessionObject ? sessionObject.sessionIDs : null;
@ -249,7 +252,7 @@ const listSessionsWithDBKey = async (dbkey) => {
for (const sessionID of Object.keys(sessions || {})) { for (const sessionID of Object.keys(sessions || {})) {
try { try {
sessions[sessionID] = await exports.getSessionInfo(sessionID); sessions[sessionID] = await exports.getSessionInfo(sessionID);
} catch (err) { } catch (err:any) {
if (err.name === 'apierror') { if (err.name === 'apierror') {
console.warn(`Found bad session ${sessionID} in ${dbkey}`); console.warn(`Found bad session ${sessionID} in ${dbkey}`);
sessions[sessionID] = null; sessions[sessionID] = null;
@ -262,9 +265,11 @@ const listSessionsWithDBKey = async (dbkey) => {
return sessions; return sessions;
}; };
/** /**
* checks if a number is an int * checks if a number is an int
* @param {number|string} value * @param {number|string} value
* @return {boolean} If the value is an integer * @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);

View file

@ -38,16 +38,16 @@ exports.info = {
const getCurrentFile = () => exports.info.file_stack[exports.info.file_stack.length - 1]; 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_stack.push(exports.info.__output);
exports.info.__output = b; exports.info.__output = b;
}; };
exports._exit = (b, recursive) => { exports._exit = (b:any, recursive:boolean) => {
exports.info.__output = exports.info.__output_stack.pop(); 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.block_stack.push(name);
exports.info.__output_stack.push(exports.info.__output.get()); exports.info.__output_stack.push(exports.info.__output.get());
exports.info.__output.set(''); exports.info.__output.set('');
@ -63,11 +63,17 @@ exports.end_block = () => {
exports.info.__output.set(exports.info.__output.get().concat(args.content)); 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 = {}; if (args == null) args = {};
let basedir = __dirname; let basedir = __dirname;
let paths = []; let paths:string[] = [];
if (exports.info.file_stack.length) { if (exports.info.file_stack.length) {
basedir = path.dirname(getCurrentFile().path); basedir = path.dirname(getCurrentFile().path);

View file

@ -10,10 +10,10 @@ const settings = require('../../utils/Settings');
const util = require('util'); const util = require('util');
const webaccess = require('./webaccess'); const webaccess = require('./webaccess');
exports.expressPreSession = async (hookName, {app}) => { exports.expressPreSession = async (hookName:string, {app}:any) => {
// This endpoint is intended to conform to: // This endpoint is intended to conform to:
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html // 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.set('Content-Type', 'application/health+json');
res.json({ res.json({
status: 'pass', 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()); 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})); 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 = let filePath =
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt'); 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 // there is no custom robots.txt, send the default robots.txt which dissallows all
if (err) { if (err) {
filePath = path.join(settings.root, 'src', 'static', 'robots.txt'); 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 () => { (async () => {
/* /*
If this is a url we simply redirect to that one. 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 / // 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})); res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req}));
}); });
// serve pad.html under /p // 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 // The below might break for pads being rewritten
const isReadOnly = !webaccess.userCanModify(req.params.pad, req); const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
@ -99,7 +99,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
}); });
// serve timeslider.html under /p/$padname/timeslider // 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', { hooks.callAll('padInitToolbar', {
toolbar, 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 // 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. // 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. // express-session automatically calls req.session.touch() so we don't need to do it here.
res.json({status: 'ok'}); res.json({status: 'ok'});
}); });

View file

@ -1,5 +1,8 @@
'use strict'; 'use strict';
import {MapArrayType} from "../../types/MapType";
import {PartType} from "../../types/PartType";
const fs = require('fs').promises; const fs = require('fs').promises;
const minify = require('../../utils/Minify'); const minify = require('../../utils/Minify');
const path = require('path'); 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. // Rewrite tar to include modules with no extensions and proper rooted paths.
const getTar = async () => { const getTar = async () => {
const prefixLocalLibraryPath = (path) => { const prefixLocalLibraryPath = (path:string) => {
if (path.charAt(0) === '$') { if (path.charAt(0) === '$') {
return path.slice(1); return path.slice(1);
} else { } else {
return `ep_etherpad-lite/static/js/${path}`; return `ep_etherpad-lite/static/js/${path}`;
} }
}; };
const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8'); const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8');
const tar = {}; const tar:MapArrayType<string[]> = {};
for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) { for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson)) as [string, string[]][]) {
const files = relativeFiles.map(prefixLocalLibraryPath); const files = relativeFiles.map(prefixLocalLibraryPath);
tar[prefixLocalLibraryPath(key)] = files tar[prefixLocalLibraryPath(key)] = files
.concat(files.map((p) => p.replace(/\.js$/, ''))) .concat(files.map((p) => p.replace(/\.js$/, '')))
@ -28,7 +32,7 @@ const getTar = async () => {
return tar; return tar;
}; };
exports.expressPreSession = async (hookName, {app}) => { exports.expressPreSession = async (hookName:string, {app}:any) => {
// Cache both minified and static. // Cache both minified and static.
const assetCache = new CachingMiddleware(); const assetCache = new CachingMiddleware();
app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache)); app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache));
@ -58,11 +62,13 @@ exports.expressPreSession = async (hookName, {app}) => {
// serve plugin definitions // serve plugin definitions
// not very static, but served here so that client can do // not very static, but served here so that client can do
// require("pluginfw/static/js/plugin-definitions.js"); // require("pluginfw/static/js/plugin-definitions.js");
app.get('/pluginfw/plugin-definitions.json', (req, res, next) => { app.get('/pluginfw/plugin-definitions.json', (req: any, res:any, next:Function) => {
const clientParts = plugins.parts.filter((part) => part.client_hooks != null); const clientParts = plugins.parts.filter((part: PartType) => part.client_hooks != null);
const clientPlugins = {}; const clientPlugins:MapArrayType<string> = {};
for (const name of new Set(clientParts.map((part) => part.plugin))) { for (const name of new Set(clientParts.map((part: PartType) => part.plugin))) {
// @ts-ignore
clientPlugins[name] = {...plugins.plugins[name]}; clientPlugins[name] = {...plugins.plugins[name]};
// @ts-ignore
delete clientPlugins[name].package; delete clientPlugins[name].package;
} }
res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Content-Type', 'application/json; charset=utf-8');

View file

@ -1,5 +1,8 @@
'use strict'; 'use strict';
import {Dirent} from "node:fs";
import {PluginDef} from "../../types/PartType";
const path = require('path'); const path = require('path');
const fsp = require('fs').promises; const fsp = require('fs').promises;
const plugins = require('../../../static/js/pluginfw/plugin_defs'); 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 '/' // Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/'
// instead of path.sep to separate pathname components. // instead of path.sep to separate pathname components.
const findSpecs = async (specDir) => { const findSpecs = async (specDir: string) => {
let dirents; let dirents: Dirent[];
try { try {
dirents = await fsp.readdir(specDir, {withFileTypes: true}); dirents = await fsp.readdir(specDir, {withFileTypes: true});
} catch (err) { } catch (err:any) {
if (['ENOENT', 'ENOTDIR'].includes(err.code)) return []; if (['ENOENT', 'ENOTDIR'].includes(err.code)) return [];
throw err; throw err;
} }
const specs = []; const specs: string[] = [];
await Promise.all(dirents.map(async (dirent) => { await Promise.all(dirents.map(async (dirent) => {
if (dirent.isDirectory()) { if (dirent.isDirectory()) {
const subdirSpecs = await findSpecs(path.join(specDir, dirent.name)); const subdirSpecs = await findSpecs(path.join(specDir, dirent.name));
@ -29,12 +32,12 @@ const findSpecs = async (specDir) => {
return specs; return specs;
}; };
exports.expressPreSession = async (hookName, {app}) => { exports.expressPreSession = async (hookName:string, {app}:any) => {
app.get('/tests/frontend/frontendTestSpecs.json', (req, res, next) => { app.get('/tests/frontend/frontendTestSpecs.json', (req:any, res:any, next:Function) => {
(async () => { (async () => {
const modules = []; const modules:string[] = [];
await Promise.all(Object.entries(plugins.plugins).map(async ([plugin, def]) => { 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; if (!pluginPath.endsWith(path.sep)) pluginPath += path.sep;
const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`; const specDir = `${plugin === 'ep_etherpad-lite' ? '' : 'static/'}tests/frontend/specs`;
for (const spec of await findSpecs(path.join(pluginPath, specDir))) { 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/'); 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('?')); res.redirect(['./', ...req.url.split('?').slice(1)].join('?'));
}); });
// The regexp /[\d\D]{0,}/ is equivalent to the regexp /.*/. The Express route path used here // 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 // 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. // 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 () => { (async () => {
let file = sanitizePathname(req.params.file); let file = sanitizePathname(req.params.file);
if (['', '.', './'].includes(file)) file = 'index.html'; if (['', '.', './'].includes(file)) file = 'index.html';
@ -74,7 +77,7 @@ exports.expressPreSession = async (hookName, {app}) => {
})().catch((err) => next(err || new Error(err))); })().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('?')); res.redirect(['./frontend/', ...req.url.split('?').slice(1)].join('?'));
}); });
}; };

View file

@ -1,16 +1,44 @@
import {MapArrayType} from "./MapType";
export type PadType = { export type PadType = {
id: string,
apool: ()=>APool, apool: ()=>APool,
atext: AText, atext: AText,
getInternalRevisionAText: (text:string)=>Promise<AText> pool: APool,
getInternalRevisionAText: (text:string)=>Promise<AText>,
getValidRevisionRange: (fromRev: string, toRev: string)=>PadRange,
getRevision: (rev?: string)=>Promise<any>,
head: number,
getAllAuthorColors: ()=>Promise<MapArrayType<string>>,
} }
type APool = { type PadRange = {
putAttrib: ([],flag: boolean)=>number startRev: string,
endRev: string,
}
export type APool = {
putAttrib: ([],flag?: boolean)=>number,
numToAttrib: MapArrayType<any>,
toJsonable: ()=>any,
clone: ()=>APool,
check: ()=>Promise<void>,
eachAttrib: (callback: (key: string, value: any)=>void)=>void,
} }
export type AText = { export type AText = {
text: string, text: string,
attribs: any attribs: any
} }
export type PadAuthor = {
}
export type AChangeSet = {
}

View file

@ -0,0 +1,10 @@
export type PartType = {
plugin: string,
client_hooks:any
}
export type PluginDef = {
package:{
path:string
}
}

View file

@ -0,0 +1,5 @@
export type UserSettingsObject = {
canCreate: boolean,
readOnly: boolean,
padAuthorizations: any
}

View file

@ -1,4 +1,7 @@
'use strict'; 'use strict';
import {AText, PadType} from "../types/PadType";
import {MapArrayType} from "../types/MapType";
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -26,7 +29,7 @@ const _analyzeLine = require('./ExportHelper')._analyzeLine;
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace; const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
const padutils = require('../../static/js/pad_utils').padutils; const padutils = require('../../static/js/pad_utils').padutils;
const getPadHTML = async (pad, revNum) => { const getPadHTML = async (pad: PadType, revNum: string) => {
let atext = pad.atext; let atext = pad.atext;
// fetch revision atext // fetch revision atext
@ -38,7 +41,7 @@ const getPadHTML = async (pad, revNum) => {
return await getHTMLFromAtext(pad, atext); 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 apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n'); const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
@ -48,7 +51,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
await Promise.all([ await Promise.all([
// prepare tags stored as ['tag', true] to be exported // 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) => { newProps.forEach((prop) => {
tags.push(prop); tags.push(prop);
props.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 // prepare tags stored as ['tag', 'value'] to be exported. This will generate HTML with tags
// like <span data-tag="value"> // like <span data-tag="value">
hooks.aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps) => { hooks.aCallAll('exportHtmlAdditionalTagsWithData', pad).then((newProps: string[]) => {
newProps.forEach((prop) => { newProps.forEach((prop) => {
tags.push(`span data-${prop[0]}="${prop[1]}"`); tags.push(`span data-${prop[0]}="${prop[1]}"`);
props.push(prop); props.push(prop);
@ -68,10 +71,10 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
// and maps them to an index in props // and maps them to an index in props
// *3:2 -> the attribute *3 means strong // *3:2 -> the attribute *3 means strong
// *2:5 -> the attribute *2 means s(trikethrough) // *2:5 -> the attribute *2 means s(trikethrough)
const anumMap = {}; const anumMap:MapArrayType<number> = {};
let css = ''; let css = '';
const stripDotFromAuthorID = (id) => id.replace(/\./g, '_'); const stripDotFromAuthorID = (id: string) => id.replace(/\./g, '_');
if (authorColors) { if (authorColors) {
css += '<style>\n'; css += '<style>\n';
@ -118,7 +121,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
} }
}); });
const getLineHTML = (text, attribs) => { const getLineHTML = (text: string, attribs: string[]) => {
// Use order of tags (b/i/u) as order of nesting, for simplicity // Use order of tags (b/i/u) as order of nesting, for simplicity
// and decent nesting. For example, // and decent nesting. For example,
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i> // <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
@ -126,12 +129,13 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i> // <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
const taker = Changeset.stringIterator(text); const taker = Changeset.stringIterator(text);
const assem = Changeset.stringAssembler(); const assem = Changeset.stringAssembler();
const openTags = []; const openTags:string[] = [];
const getSpanClassFor = (i) => { const getSpanClassFor = (i: string) => {
// return if author colors are disabled // return if author colors are disabled
if (!authorColors) return false; if (!authorColors) return false;
// @ts-ignore
const property = props[i]; const property = props[i];
// we are not insterested on properties in the form of ['color', 'red'], // we are not insterested on properties in the form of ['color', 'red'],
@ -153,12 +157,13 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
// tags added by exportHtmlAdditionalTagsWithData will be exported as <span> with // tags added by exportHtmlAdditionalTagsWithData will be exported as <span> with
// data attributes // data attributes
const isSpanWithData = (i) => { const isSpanWithData = (i: string) => {
// @ts-ignore
const property = props[i]; const property = props[i];
return Array.isArray(property); return Array.isArray(property);
}; };
const emitOpenTag = (i) => { const emitOpenTag = (i: string) => {
openTags.unshift(i); openTags.unshift(i);
const spanClass = getSpanClassFor(i); const spanClass = getSpanClassFor(i);
@ -168,13 +173,14 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
assem.append('">'); assem.append('">');
} else { } else {
assem.append('<'); assem.append('<');
// @ts-ignore
assem.append(tags[i]); assem.append(tags[i]);
assem.append('>'); assem.append('>');
} }
}; };
// this closes an open tag and removes its reference from openTags // this closes an open tag and removes its reference from openTags
const emitCloseTag = (i) => { const emitCloseTag = (i: string) => {
openTags.shift(); openTags.shift();
const spanClass = getSpanClassFor(i); const spanClass = getSpanClassFor(i);
const spanWithData = isSpanWithData(i); const spanWithData = isSpanWithData(i);
@ -183,6 +189,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
assem.append('</span>'); assem.append('</span>');
} else { } else {
assem.append('</'); assem.append('</');
// @ts-ignore
assem.append(tags[i]); assem.append(tags[i]);
assem.append('>'); assem.append('>');
} }
@ -192,7 +199,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
let idx = 0; let idx = 0;
const processNextChars = (numChars) => { const processNextChars = (numChars: number) => {
if (numChars <= 0) { if (numChars <= 0) {
return; return;
} }
@ -203,12 +210,12 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
// this iterates over every op string and decides which tags to open or to close // this iterates over every op string and decides which tags to open or to close
// based on the attribs used // based on the attribs used
for (const o of ops) { for (const o of ops) {
const usedAttribs = []; const usedAttribs:string[] = [];
// mark all attribs as used // mark all attribs as used
for (const a of attributes.decodeAttribString(o.attribs)) { for (const a of attributes.decodeAttribString(o.attribs)) {
if (a in anumMap) { if (a in anumMap) {
usedAttribs.push(anumMap[a]); // i = 0 => bold, etc. usedAttribs.push(String(anumMap[a])); // i = 0 => bold, etc.
} }
} }
let outermostTag = -1; let outermostTag = -1;
@ -256,7 +263,9 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
}; };
// end processNextChars // end processNextChars
if (urls) { if (urls) {
urls.forEach((urlData) => { urls.forEach((urlData: [number, {
length: number,
}]) => {
const startIndex = urlData[0]; const startIndex = urlData[0];
const url = urlData[1]; const url = urlData[1];
const urlLength = url.length; const urlLength = url.length;
@ -288,7 +297,13 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
// so we want to do something reasonable there. We also // so we want to do something reasonable there. We also
// want to deal gracefully with blank lines. // want to deal gracefully with blank lines.
// => keeps track of the parents level of indentation // => keeps track of the parents level of indentation
let openLists = [];
type openList = {
level: number,
type: string,
}
let openLists: openList[] = [];
for (let i = 0; i < textLines.length; i++) { for (let i = 0; i < textLines.length; i++) {
let context; let context;
const line = _analyzeLine(textLines[i], attribLines[i], apool); const line = _analyzeLine(textLines[i], attribLines[i], apool);
@ -315,7 +330,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
// To create list parent elements // To create list parent elements
if ((!prevLine || prevLine.listLevel !== line.listLevel) || if ((!prevLine || prevLine.listLevel !== line.listLevel) ||
(line.listTypeName !== prevLine.listTypeName)) { (line.listTypeName !== prevLine.listTypeName)) {
const exists = _.find(openLists, (item) => ( const exists = _.find(openLists, (item:openList) => (
item.level === line.listLevel && item.type === line.listTypeName)); item.level === line.listLevel && item.type === line.listTypeName));
if (!exists) { if (!exists) {
let prevLevel = 0; let prevLevel = 0;
@ -456,12 +471,12 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
return pieces.join(''); return pieces.join('');
}; };
exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => { exports.getPadHTMLDocument = async (padId: string, revNum: string, readOnlyId: number) => {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
// Include some Styles into the Head for Export // Include some Styles into the Head for Export
let stylesForExportCSS = ''; let stylesForExportCSS = '';
const stylesForExport = await hooks.aCallAll('stylesForExport', padId); const stylesForExport: string[] = await hooks.aCallAll('stylesForExport', padId);
stylesForExport.forEach((css) => { stylesForExport.forEach((css) => {
stylesForExportCSS += css; stylesForExportCSS += css;
}); });
@ -480,7 +495,7 @@ exports.getPadHTMLDocument = async (padId, revNum, readOnlyId) => {
}; };
// copied from ACE // copied from ACE
const _processSpaces = (s) => { const _processSpaces = (s: string) => {
const doesWrap = true; const doesWrap = true;
if (s.indexOf('<') < 0 && !doesWrap) { if (s.indexOf('<') < 0 && !doesWrap) {
// short-cut // short-cut
@ -489,6 +504,7 @@ const _processSpaces = (s) => {
const parts = []; const parts = [];
s.replace(/<[^>]*>?| |[^ <]+/g, (m) => { s.replace(/<[^>]*>?| |[^ <]+/g, (m) => {
parts.push(m); parts.push(m);
return m
}); });
if (doesWrap) { if (doesWrap) {
let endOfLine = true; let endOfLine = true;

View file

@ -27,8 +27,14 @@ const settings = require('./Settings');
const logger = log4js.getLogger('LibreOffice'); const logger = log4js.getLogger('LibreOffice');
const doConvertTask = async (task) => { const doConvertTask = async (task:{
type: string,
srcFile: string,
fileExtension: string,
destFile: string,
}) => {
const tmpDir = os.tmpdir(); const tmpDir = os.tmpdir();
// @ts-ignore
const p = runCmd([ const p = runCmd([
settings.soffice, settings.soffice,
'--headless', '--headless',
@ -43,8 +49,10 @@ const doConvertTask = async (task) => {
tmpDir, tmpDir,
], {stdio: [ ], {stdio: [
null, null,
(line) => logger.info(`[${p.child.pid}] stdout: ${line}`), // @ts-ignore
(line) => logger.error(`[${p.child.pid}] stderr: ${line}`), (line) => logger.info(`[${p.child.pid}] stdout: ${line}`),
// @ts-ignore
(line) => logger.error(`[${p.child.pid}] stderr: ${line}`),
]}); ]});
logger.info(`[${p.child.pid}] Converting ${task.srcFile} to ${task.type} in ${tmpDir}`); logger.info(`[${p.child.pid}] Converting ${task.srcFile} to ${task.type} in ${tmpDir}`);
// Soffice/libreoffice is buggy and often hangs. // Soffice/libreoffice is buggy and often hangs.
@ -56,7 +64,7 @@ const doConvertTask = async (task) => {
}, 120000); }, 120000);
try { try {
await p; await p;
} catch (err) { } catch (err:any) {
logger.error(`[${p.child.pid}] Conversion failed: ${err.stack || err}`); logger.error(`[${p.child.pid}] Conversion failed: ${err.stack || err}`);
throw err; throw err;
} finally { } finally {
@ -81,7 +89,7 @@ const queue = async.queue(doConvertTask, 1);
* @param {String} type The type to convert into * @param {String} type The type to convert into
* @param {Function} callback Standard callback function * @param {Function} callback Standard callback function
*/ */
exports.convertFile = async (srcFile, destFile, type) => { exports.convertFile = async (srcFile: string, destFile: string, type:string) => {
// Used for the moving of the file, not the conversion // Used for the moving of the file, not the conversion
const fileExtension = type; const fileExtension = type;

View file

@ -26,7 +26,7 @@ const semver = require('semver');
* *
* @param {String} minNodeVersion Minimum required Node version * @param {String} minNodeVersion Minimum required Node version
*/ */
exports.enforceMinNodeVersion = (minNodeVersion) => { exports.enforceMinNodeVersion = (minNodeVersion: string) => {
const currentNodeVersion = process.version; const currentNodeVersion = process.version;
// we cannot use template literals, since we still do not know if we are // we cannot use template literals, since we still do not know if we are
@ -49,7 +49,7 @@ exports.enforceMinNodeVersion = (minNodeVersion) => {
* @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated * @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated
* Node releases * Node releases
*/ */
exports.checkDeprecationStatus = (lowestNonDeprecatedNodeVersion, epRemovalVersion) => { exports.checkDeprecationStatus = (lowestNonDeprecatedNodeVersion: string, epRemovalVersion:Function) => {
const currentNodeVersion = process.version; const currentNodeVersion = process.version;
if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) { if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) {

View file

@ -50,13 +50,13 @@ const nonSettings = [
// This is a function to make it easy to create a new instance. It is important to not reuse a // This is a function to make it easy to create a new instance. It is important to not reuse a
// config object after passing it to log4js.configure() because that method mutates the object. :( // config object after passing it to log4js.configure() because that method mutates the object. :(
const defaultLogConfig = (level) => ({appenders: {console: {type: 'console'}}, const defaultLogConfig = (level:string) => ({appenders: {console: {type: 'console'}},
categories: { categories: {
default: {appenders: ['console'], level}, default: {appenders: ['console'], level},
}}); }});
const defaultLogLevel = 'INFO'; const defaultLogLevel = 'INFO';
const initLogging = (logLevel, config) => { const initLogging = (config:any) => {
// log4js.configure() modifies exports.logconfig so check for equality first. // log4js.configure() modifies exports.logconfig so check for equality first.
log4js.configure(config); log4js.configure(config);
log4js.getLogger('console'); log4js.getLogger('console');
@ -70,7 +70,7 @@ const initLogging = (logLevel, config) => {
// Initialize logging as early as possible with reasonable defaults. Logging will be re-initialized // Initialize logging as early as possible with reasonable defaults. Logging will be re-initialized
// with the user's chosen log level and logger config after the settings have been loaded. // with the user's chosen log level and logger config after the settings have been loaded.
initLogging(defaultLogLevel, defaultLogConfig(defaultLogLevel)); initLogging(defaultLogConfig(defaultLogLevel));
/* Root path of the installation */ /* Root path of the installation */
exports.root = absolutePaths.findEtherpadRoot(); exports.root = absolutePaths.findEtherpadRoot();
@ -487,7 +487,7 @@ exports.getGitCommit = () => {
version = ref; version = ref;
} }
version = version.substring(0, 7); version = version.substring(0, 7);
} catch (e) { } catch (e:any) {
logger.warn(`Can't get git version for server header\n${e.message}`); logger.warn(`Can't get git version for server header\n${e.message}`);
} }
return version; return version;
@ -503,7 +503,7 @@ exports.getEpVersion = () => require('../../package.json').version;
* This code refactors a previous version that copied & pasted the same code for * This code refactors a previous version that copied & pasted the same code for
* both "settings.json" and "credentials.json". * both "settings.json" and "credentials.json".
*/ */
const storeSettings = (settingsObj) => { const storeSettings = (settingsObj:any) => {
for (const i of Object.keys(settingsObj || {})) { for (const i of Object.keys(settingsObj || {})) {
if (nonSettings.includes(i)) { if (nonSettings.includes(i)) {
logger.warn(`Ignoring setting: '${i}'`); logger.warn(`Ignoring setting: '${i}'`);
@ -542,8 +542,9 @@ const storeSettings = (settingsObj) => {
* short syntax "${ABIWORD}", and not "${ABIWORD:null}": the latter would result * short syntax "${ABIWORD}", and not "${ABIWORD:null}": the latter would result
* in the literal string "null", instead. * in the literal string "null", instead.
*/ */
const coerceValue = (stringValue) => { const coerceValue = (stringValue:string) => {
// cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number // cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number
// @ts-ignore
const isNumeric = !isNaN(stringValue) && !isNaN(parseFloat(stringValue) && isFinite(stringValue)); const isNumeric = !isNaN(stringValue) && !isNaN(parseFloat(stringValue) && isFinite(stringValue));
if (isNumeric) { if (isNumeric) {
@ -597,7 +598,7 @@ const coerceValue = (stringValue) => {
* *
* see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter * see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter
*/ */
const lookupEnvironmentVariables = (obj) => { const lookupEnvironmentVariables = (obj: object) => {
const stringifiedAndReplaced = JSON.stringify(obj, (key, value) => { const stringifiedAndReplaced = JSON.stringify(obj, (key, value) => {
/* /*
* the first invocation of replacer() is with an empty key. Just go on, or * the first invocation of replacer() is with an empty key. Just go on, or
@ -669,7 +670,7 @@ const lookupEnvironmentVariables = (obj) => {
logger.debug( logger.debug(
`Configuration key "${key}" will be read from environment variable "${envVarName}"`); `Configuration key "${key}" will be read from environment variable "${envVarName}"`);
return coerceValue(envVarValue); return coerceValue(envVarValue!);
}); });
const newSettings = JSON.parse(stringifiedAndReplaced); const newSettings = JSON.parse(stringifiedAndReplaced);
@ -685,7 +686,7 @@ const lookupEnvironmentVariables = (obj) => {
* *
* The isSettings variable only controls the error logging. * The isSettings variable only controls the error logging.
*/ */
const parseSettings = (settingsFilename, isSettings) => { const parseSettings = (settingsFilename:string, isSettings:boolean) => {
let settingsStr = ''; let settingsStr = '';
let settingsType, notFoundMessage, notFoundFunction; let settingsType, notFoundMessage, notFoundFunction;
@ -720,7 +721,7 @@ const parseSettings = (settingsFilename, isSettings) => {
const replacedSettings = lookupEnvironmentVariables(settings); const replacedSettings = lookupEnvironmentVariables(settings);
return replacedSettings; return replacedSettings;
} catch (e) { } catch (e:any) {
logger.error(`There was an error processing your ${settingsType} ` + logger.error(`There was an error processing your ${settingsType} ` +
`file from ${settingsFilename}: ${e.message}`); `file from ${settingsFilename}: ${e.message}`);
@ -736,7 +737,7 @@ exports.reloadSettings = () => {
// Init logging config // Init logging config
exports.logconfig = defaultLogConfig(exports.loglevel ? exports.loglevel : defaultLogLevel); exports.logconfig = defaultLogConfig(exports.loglevel ? exports.loglevel : defaultLogLevel);
initLogging(exports.loglevel, exports.logconfig); initLogging(exports.logconfig);
if (!exports.skinName) { if (!exports.skinName) {
logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' + logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' +
@ -780,7 +781,7 @@ exports.reloadSettings = () => {
if (exports.abiword) { if (exports.abiword) {
// Check abiword actually exists // Check abiword actually exists
if (exports.abiword != null) { if (exports.abiword != null) {
fs.exists(exports.abiword, (exists) => { fs.exists(exports.abiword, (exists: boolean) => {
if (!exists) { if (!exists) {
const abiwordError = 'Abiword does not exist at this path, check your settings file.'; const abiwordError = 'Abiword does not exist at this path, check your settings file.';
if (!exports.suppressErrorsInPadText) { if (!exports.suppressErrorsInPadText) {
@ -794,7 +795,7 @@ exports.reloadSettings = () => {
} }
if (exports.soffice) { if (exports.soffice) {
fs.exists(exports.soffice, (exists) => { fs.exists(exports.soffice, (exists: boolean) => {
if (!exists) { if (!exists) {
const sofficeError = const sofficeError =
'soffice (libreoffice) does not exist at this path, check your settings file.'; 'soffice (libreoffice) does not exist at this path, check your settings file.';

View file

@ -1,447 +0,0 @@
'use strict';
const AttributeMap = require('../../static/js/AttributeMap');
const Changeset = require('../../static/js/Changeset');
const attributes = require('../../static/js/attributes');
const exportHtml = require('./ExportHtml');
function PadDiff(pad, fromRev, toRev) {
// check parameters
if (!pad || !pad.id || !pad.atext || !pad.pool) {
throw new Error('Invalid pad');
}
const range = pad.getValidRevisionRange(fromRev, toRev);
if (!range) throw new Error(`Invalid revision range. startRev: ${fromRev} endRev: ${toRev}`);
this._pad = pad;
this._fromRev = range.startRev;
this._toRev = range.endRev;
this._html = null;
this._authors = [];
}
PadDiff.prototype._isClearAuthorship = function (changeset) {
// unpack
const unpacked = Changeset.unpack(changeset);
// check if there is nothing in the charBank
if (unpacked.charBank !== '') {
return false;
}
// check if oldLength == newLength
if (unpacked.oldLen !== unpacked.newLen) {
return false;
}
const [clearOperator, anotherOp] = Changeset.deserializeOps(unpacked.ops);
// check if there is only one operator
if (anotherOp != null) return false;
// check if this operator doesn't change text
if (clearOperator.opcode !== '=') {
return false;
}
// check that this operator applys to the complete text
// if the text ends with a new line, its exactly one character less, else it has the same length
if (clearOperator.chars !== unpacked.oldLen - 1 && clearOperator.chars !== unpacked.oldLen) {
return false;
}
const [appliedAttribute, anotherAttribute] =
attributes.attribsFromString(clearOperator.attribs, this._pad.pool);
// Check that the operation has exactly one attribute.
if (appliedAttribute == null || anotherAttribute != null) return false;
// check if the applied attribute is an anonymous author attribute
if (appliedAttribute[0] !== 'author' || appliedAttribute[1] !== '') {
return false;
}
return true;
};
PadDiff.prototype._createClearAuthorship = async function (rev) {
const atext = await this._pad.getInternalRevisionAText(rev);
// build clearAuthorship changeset
const builder = Changeset.builder(atext.text.length);
builder.keepText(atext.text, [['author', '']], this._pad.pool);
const changeset = builder.toString();
return changeset;
};
PadDiff.prototype._createClearStartAtext = async function (rev) {
// get the atext of this revision
const atext = await this._pad.getInternalRevisionAText(rev);
// create the clearAuthorship changeset
const changeset = await this._createClearAuthorship(rev);
// apply the clearAuthorship changeset
const newAText = Changeset.applyToAText(changeset, atext, this._pad.pool);
return newAText;
};
PadDiff.prototype._getChangesetsInBulk = async function (startRev, count) {
// find out which revisions we need
const revisions = [];
for (let i = startRev; i < (startRev + count) && i <= this._pad.head; i++) {
revisions.push(i);
}
// get all needed revisions (in parallel)
const changesets = []; const
authors = [];
await Promise.all(revisions.map((rev) => this._pad.getRevision(rev).then((revision) => {
const arrayNum = rev - startRev;
changesets[arrayNum] = revision.changeset;
authors[arrayNum] = revision.meta.author;
})));
return {changesets, authors};
};
PadDiff.prototype._addAuthors = function (authors) {
const self = this;
// add to array if not in the array
authors.forEach((author) => {
if (self._authors.indexOf(author) === -1) {
self._authors.push(author);
}
});
};
PadDiff.prototype._createDiffAtext = async function () {
const bulkSize = 100;
// get the cleaned startAText
let atext = await this._createClearStartAtext(this._fromRev);
let superChangeset = null;
for (let rev = this._fromRev + 1; rev <= this._toRev; rev += bulkSize) {
// get the bulk
const {changesets, authors} = await this._getChangesetsInBulk(rev, bulkSize);
const addedAuthors = [];
// run through all changesets
for (let i = 0; i < changesets.length && (rev + i) <= this._toRev; ++i) {
let changeset = changesets[i];
// skip clearAuthorship Changesets
if (this._isClearAuthorship(changeset)) {
continue;
}
changeset = this._extendChangesetWithAuthor(changeset, authors[i], this._pad.pool);
// add this author to the authorarray
addedAuthors.push(authors[i]);
// compose it with the superChangset
if (superChangeset == null) {
superChangeset = changeset;
} else {
superChangeset = Changeset.compose(superChangeset, changeset, this._pad.pool);
}
}
// add the authors to the PadDiff authorArray
this._addAuthors(addedAuthors);
}
// if there are only clearAuthorship changesets, we don't get a superChangeset,
// so we can skip this step
if (superChangeset) {
const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool);
// apply the superChangeset, which includes all addings
atext = Changeset.applyToAText(superChangeset, atext, this._pad.pool);
// apply the deletionChangeset, which adds a deletions
atext = Changeset.applyToAText(deletionChangeset, atext, this._pad.pool);
}
return atext;
};
PadDiff.prototype.getHtml = async function () {
// cache the html
if (this._html != null) {
return this._html;
}
// get the diff atext
const atext = await this._createDiffAtext();
// get the authorColor table
const authorColors = await this._pad.getAllAuthorColors();
// convert the atext to html
this._html = await exportHtml.getHTMLFromAtext(this._pad, atext, authorColors);
return this._html;
};
PadDiff.prototype.getAuthors = async function () {
// check if html was already produced, if not produce it, this generates
// the author array at the same time
if (this._html == null) {
await this.getHtml();
}
return self._authors;
};
PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => {
// unpack
const unpacked = Changeset.unpack(changeset);
const assem = Changeset.opAssembler();
// create deleted attribs
const authorAttrib = apool.putAttrib(['author', author || '']);
const deletedAttrib = apool.putAttrib(['removed', true]);
const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`;
for (const operator of Changeset.deserializeOps(unpacked.ops)) {
if (operator.opcode === '-') {
// this is a delete operator, extend it with the author
operator.attribs = attribs;
} else if (operator.opcode === '=' && operator.attribs) {
// this is operator changes only attributes, let's mark which author did that
operator.attribs += `*${Changeset.numToString(authorAttrib)}`;
}
// append the new operator to our assembler
assem.append(operator);
}
// return the modified changeset
return Changeset.pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank);
};
// this method is 80% like Changeset.inverse. I just changed so instead of reverting,
// it adds deletions and attribute changes to to the atext.
PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
const lines = Changeset.splitTextLines(startAText.text);
const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text);
// lines and alines are what the exports is meant to apply to.
// They may be arrays or objects with .get(i) and .length methods.
// They include final newlines on lines.
const linesGet = (idx) => {
if (lines.get) {
return lines.get(idx);
} else {
return lines[idx];
}
};
const aLinesGet = (idx) => {
if (alines.get) {
return alines.get(idx);
} else {
return alines[idx];
}
};
let curLine = 0;
let curChar = 0;
let curLineOps = null;
let curLineOpsNext = null;
let curLineOpsLine;
let curLineNextOp = new Changeset.Op('+');
const unpacked = Changeset.unpack(cs);
const builder = Changeset.builder(unpacked.newLen);
const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => {
if (!curLineOps || curLineOpsLine !== curLine) {
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
curLineOpsNext = curLineOps.next();
curLineOpsLine = curLine;
let indexIntoLine = 0;
while (!curLineOpsNext.done) {
curLineNextOp = curLineOpsNext.value;
curLineOpsNext = curLineOps.next();
if (indexIntoLine + curLineNextOp.chars >= curChar) {
curLineNextOp.chars -= (curChar - indexIntoLine);
break;
}
indexIntoLine += curLineNextOp.chars;
}
}
while (numChars > 0) {
if (!curLineNextOp.chars && curLineOpsNext.done) {
curLine++;
curChar = 0;
curLineOpsLine = curLine;
curLineNextOp.chars = 0;
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
curLineOpsNext = curLineOps.next();
}
if (!curLineNextOp.chars) {
if (curLineOpsNext.done) {
curLineNextOp = new Changeset.Op();
} else {
curLineNextOp = curLineOpsNext.value;
curLineOpsNext = curLineOps.next();
}
}
const charsToUse = Math.min(numChars, curLineNextOp.chars);
func(charsToUse, curLineNextOp.attribs,
charsToUse === curLineNextOp.chars && curLineNextOp.lines > 0);
numChars -= charsToUse;
curLineNextOp.chars -= charsToUse;
curChar += charsToUse;
}
if (!curLineNextOp.chars && curLineOpsNext.done) {
curLine++;
curChar = 0;
}
};
const skip = (N, L) => {
if (L) {
curLine += L;
curChar = 0;
} else if (curLineOps && curLineOpsLine === curLine) {
consumeAttribRuns(N, () => {});
} else {
curChar += N;
}
};
const nextText = (numChars) => {
let len = 0;
const assem = Changeset.stringAssembler();
const firstString = linesGet(curLine).substring(curChar);
len += firstString.length;
assem.append(firstString);
let lineNum = curLine + 1;
while (len < numChars) {
const nextString = linesGet(lineNum);
len += nextString.length;
assem.append(nextString);
lineNum++;
}
return assem.toString().substring(0, numChars);
};
const cachedStrFunc = (func) => {
const cache = {};
return (s) => {
if (!cache[s]) {
cache[s] = func(s);
}
return cache[s];
};
};
for (const csOp of Changeset.deserializeOps(unpacked.ops)) {
if (csOp.opcode === '=') {
const textBank = nextText(csOp.chars);
// decide if this equal operator is an attribution change or not.
// We can see this by checkinf if attribs is set.
// If the text this operator applies to is only a star,
// than this is a false positive and should be ignored
if (csOp.attribs && textBank !== '*') {
const attribs = AttributeMap.fromString(csOp.attribs, apool);
const undoBackToAttribs = cachedStrFunc((oldAttribsStr) => {
const oldAttribs = AttributeMap.fromString(oldAttribsStr, apool);
const backAttribs = new AttributeMap(apool)
.set('author', '')
.set('removed', 'true');
for (const [key, value] of attribs) {
const oldValue = oldAttribs.get(key);
if (oldValue !== value) backAttribs.set(key, oldValue);
}
// TODO: backAttribs does not restore removed attributes (it is missing attributes that
// are in oldAttribs but not in attribs). I don't know if that is intentional.
return backAttribs.toString();
});
let textLeftToProcess = textBank;
while (textLeftToProcess.length > 0) {
// process till the next line break or process only one line break
let lengthToProcess = textLeftToProcess.indexOf('\n');
let lineBreak = false;
switch (lengthToProcess) {
case -1:
lengthToProcess = textLeftToProcess.length;
break;
case 0:
lineBreak = true;
lengthToProcess = 1;
break;
}
// get the text we want to procceed in this step
const processText = textLeftToProcess.substr(0, lengthToProcess);
textLeftToProcess = textLeftToProcess.substr(lengthToProcess);
if (lineBreak) {
builder.keep(1, 1); // just skip linebreaks, don't do a insert + keep for a linebreak
// consume the attributes of this linebreak
consumeAttribRuns(1, () => {});
} else {
// add the old text via an insert, but add a deletion attribute +
// the author attribute of the author who deleted it
let textBankIndex = 0;
consumeAttribRuns(lengthToProcess, (len, attribs, endsLine) => {
// get the old attributes back
const oldAttribs = undoBackToAttribs(attribs);
builder.insert(processText.substr(textBankIndex, len), oldAttribs);
textBankIndex += len;
});
builder.keep(lengthToProcess, 0);
}
}
} else {
skip(csOp.chars, csOp.lines);
builder.keep(csOp.chars, csOp.lines);
}
} else if (csOp.opcode === '+') {
builder.keep(csOp.chars, csOp.lines);
} else if (csOp.opcode === '-') {
const textBank = nextText(csOp.chars);
let textBankIndex = 0;
consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => {
builder.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs);
textBankIndex += len;
});
}
}
return Changeset.checkRep(builder.toString());
};
// export the constructor
module.exports = PadDiff;

458
src/node/utils/padDiff.ts Normal file
View file

@ -0,0 +1,458 @@
'use strict';
import {PadAuthor, PadType} from "../types/PadType";
import {MapArrayType} from "../types/MapType";
const AttributeMap = require('../../static/js/AttributeMap');
const Changeset = require('../../static/js/Changeset');
const attributes = require('../../static/js/attributes');
const exportHtml = require('./ExportHtml');
class PadDiff {
private readonly _pad: PadType;
private readonly _fromRev: string;
private readonly _toRev: string;
private _html: any;
public _authors: any[];
private self: PadDiff | undefined
constructor(pad: PadType, fromRev:string, toRev:string) {
// check parameters
if (!pad || !pad.id || !pad.atext || !pad.pool) {
throw new Error('Invalid pad');
}
const range = pad.getValidRevisionRange(fromRev, toRev);
if (!range) throw new Error(`Invalid revision range. startRev: ${fromRev} endRev: ${toRev}`);
this._pad = pad;
this._fromRev = range.startRev;
this._toRev = range.endRev;
this._html = null;
this._authors = [];
}
_isClearAuthorship(changeset: any){
// unpack
const unpacked = Changeset.unpack(changeset);
// check if there is nothing in the charBank
if (unpacked.charBank !== '') {
return false;
}
// check if oldLength == newLength
if (unpacked.oldLen !== unpacked.newLen) {
return false;
}
const [clearOperator, anotherOp] = Changeset.deserializeOps(unpacked.ops);
// check if there is only one operator
if (anotherOp != null) return false;
// check if this operator doesn't change text
if (clearOperator.opcode !== '=') {
return false;
}
// check that this operator applys to the complete text
// if the text ends with a new line, its exactly one character less, else it has the same length
if (clearOperator.chars !== unpacked.oldLen - 1 && clearOperator.chars !== unpacked.oldLen) {
return false;
}
const [appliedAttribute, anotherAttribute] =
attributes.attribsFromString(clearOperator.attribs, this._pad.pool);
// Check that the operation has exactly one attribute.
if (appliedAttribute == null || anotherAttribute != null) return false;
// check if the applied attribute is an anonymous author attribute
if (appliedAttribute[0] !== 'author' || appliedAttribute[1] !== '') {
return false;
}
return true;
}
async _createClearAuthorship(rev: any){
const atext = await this._pad.getInternalRevisionAText(rev);
// build clearAuthorship changeset
const builder = Changeset.builder(atext.text.length);
builder.keepText(atext.text, [['author', '']], this._pad.pool);
const changeset = builder.toString();
return changeset;
}
async _createClearStartAtext(rev: any){
// get the atext of this revision
const atext = await this._pad.getInternalRevisionAText(rev);
// create the clearAuthorship changeset
const changeset = await this._createClearAuthorship(rev);
// apply the clearAuthorship changeset
const newAText = Changeset.applyToAText(changeset, atext, this._pad.pool);
return newAText;
}
async _getChangesetsInBulk(startRev: any, count: any) {
// find out which revisions we need
const revisions = [];
for (let i = startRev; i < (startRev + count) && i <= this._pad.head; i++) {
revisions.push(i);
}
// get all needed revisions (in parallel)
const changesets:any[] = [];
const authors: any[] = [];
await Promise.all(revisions.map((rev) => this._pad.getRevision(rev).then((revision) => {
const arrayNum = rev - startRev;
changesets[arrayNum] = revision.changeset;
authors[arrayNum] = revision.meta.author;
})));
return {changesets, authors};
}
_addAuthors(authors: PadAuthor[]){
this.self = this;
// add to array if not in the array
authors.forEach((author) => {
if (this.self!._authors.indexOf(author) === -1) {
this.self!._authors.push(author);
}
});
}
async _createDiffAtext(){
const bulkSize = 100;
// get the cleaned startAText
let atext = await this._createClearStartAtext(this._fromRev);
let superChangeset = null;
for (let rev = this._fromRev + 1; rev <= this._toRev; rev += bulkSize) {
// get the bulk
const {changesets, authors} = await this._getChangesetsInBulk(rev, bulkSize);
const addedAuthors = [];
// run through all changesets
for (let i = 0; i < changesets.length && (rev + i) <= this._toRev; ++i) {
let changeset = changesets[i];
// skip clearAuthorship Changesets
if (this._isClearAuthorship(changeset)) {
continue;
}
changeset = this._extendChangesetWithAuthor(changeset, authors[i], this._pad.pool);
// add this author to the authorarray
addedAuthors.push(authors[i]);
// compose it with the superChangset
if (superChangeset == null) {
superChangeset = changeset;
} else {
superChangeset = Changeset.compose(superChangeset, changeset, this._pad.pool);
}
}
// add the authors to the PadDiff authorArray
this._addAuthors(addedAuthors);
}
// if there are only clearAuthorship changesets, we don't get a superChangeset,
// so we can skip this step
if (superChangeset) {
const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool);
// apply the superChangeset, which includes all addings
atext = Changeset.applyToAText(superChangeset, atext, this._pad.pool);
// apply the deletionChangeset, which adds a deletions
atext = Changeset.applyToAText(deletionChangeset, atext, this._pad.pool);
}
return atext;
}
async getHtml(){
// cache the html
if (this._html != null) {
return this._html;
}
// get the diff atext
const atext = await this._createDiffAtext();
// get the authorColor table
const authorColors = await this._pad.getAllAuthorColors();
// convert the atext to html
this._html = await exportHtml.getHTMLFromAtext(this._pad, atext, authorColors);
return this._html;
}
async getAuthors() {
// check if html was already produced, if not produce it, this generates
// the author array at the same time
if (this._html == null) {
await this.getHtml();
}
return this.self!._authors;
}
_extendChangesetWithAuthor(changeset: any, author: any, apool: any){
// unpack
const unpacked = Changeset.unpack(changeset);
const assem = Changeset.opAssembler();
// create deleted attribs
const authorAttrib = apool.putAttrib(['author', author || '']);
const deletedAttrib = apool.putAttrib(['removed', true]);
const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`;
for (const operator of Changeset.deserializeOps(unpacked.ops)) {
if (operator.opcode === '-') {
// this is a delete operator, extend it with the author
operator.attribs = attribs;
} else if (operator.opcode === '=' && operator.attribs) {
// this is operator changes only attributes, let's mark which author did that
operator.attribs += `*${Changeset.numToString(authorAttrib)}`;
}
// append the new operator to our assembler
assem.append(operator);
}
// return the modified changeset
return Changeset.pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank);
}
_createDeletionChangeset(cs: any, startAText: any, apool: any){
const lines = Changeset.splitTextLines(startAText.text);
const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text);
// lines and alines are what the exports is meant to apply to.
// They may be arrays or objects with .get(i) and .length methods.
// They include final newlines on lines.
const linesGet = (idx: number) => {
if (lines.get) {
return lines.get(idx);
} else {
return lines[idx];
}
};
const aLinesGet = (idx: number) => {
if (alines.get) {
return alines.get(idx);
} else {
return alines[idx];
}
};
let curLine = 0;
let curChar = 0;
let curLineOps: { next: () => any; } | null = null;
let curLineOpsNext: { done: any; value: any; } | null = null;
let curLineOpsLine: number;
let curLineNextOp = new Changeset.Op('+');
const unpacked = Changeset.unpack(cs);
const builder = Changeset.builder(unpacked.newLen);
const consumeAttribRuns = (numChars: number, func: Function /* (len, attribs, endsLine)*/) => {
if (!curLineOps || curLineOpsLine !== curLine) {
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
curLineOpsNext = curLineOps!.next();
curLineOpsLine = curLine;
let indexIntoLine = 0;
while (!curLineOpsNext!.done) {
curLineNextOp = curLineOpsNext!.value;
curLineOpsNext = curLineOps!.next();
if (indexIntoLine + curLineNextOp.chars >= curChar) {
curLineNextOp.chars -= (curChar - indexIntoLine);
break;
}
indexIntoLine += curLineNextOp.chars;
}
}
while (numChars > 0) {
if (!curLineNextOp.chars && curLineOpsNext!.done) {
curLine++;
curChar = 0;
curLineOpsLine = curLine;
curLineNextOp.chars = 0;
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
curLineOpsNext = curLineOps!.next();
}
if (!curLineNextOp.chars) {
if (curLineOpsNext!.done) {
curLineNextOp = new Changeset.Op();
} else {
curLineNextOp = curLineOpsNext!.value;
curLineOpsNext = curLineOps!.next();
}
}
const charsToUse = Math.min(numChars, curLineNextOp.chars);
func(charsToUse, curLineNextOp.attribs,
charsToUse === curLineNextOp.chars && curLineNextOp.lines > 0);
numChars -= charsToUse;
curLineNextOp.chars -= charsToUse;
curChar += charsToUse;
}
if (!curLineNextOp.chars && curLineOpsNext!.done) {
curLine++;
curChar = 0;
}
};
const skip = (N:number, L:number) => {
if (L) {
curLine += L;
curChar = 0;
} else if (curLineOps && curLineOpsLine === curLine) {
consumeAttribRuns(N, () => {});
} else {
curChar += N;
}
};
const nextText = (numChars: number) => {
let len = 0;
const assem = Changeset.stringAssembler();
const firstString = linesGet(curLine).substring(curChar);
len += firstString.length;
assem.append(firstString);
let lineNum = curLine + 1;
while (len < numChars) {
const nextString = linesGet(lineNum);
len += nextString.length;
assem.append(nextString);
lineNum++;
}
return assem.toString().substring(0, numChars);
};
const cachedStrFunc = (func:Function) => {
const cache:MapArrayType<any> = {};
return (s:string) => {
if (!cache[s]) {
cache[s] = func(s);
}
return cache[s];
};
};
for (const csOp of Changeset.deserializeOps(unpacked.ops)) {
if (csOp.opcode === '=') {
const textBank = nextText(csOp.chars);
// decide if this equal operator is an attribution change or not.
// We can see this by checkinf if attribs is set.
// If the text this operator applies to is only a star,
// than this is a false positive and should be ignored
if (csOp.attribs && textBank !== '*') {
const attribs = AttributeMap.fromString(csOp.attribs, apool);
const undoBackToAttribs = cachedStrFunc((oldAttribsStr: string) => {
const oldAttribs = AttributeMap.fromString(oldAttribsStr, apool);
const backAttribs = new AttributeMap(apool)
.set('author', '')
.set('removed', 'true');
for (const [key, value] of attribs) {
const oldValue = oldAttribs.get(key);
if (oldValue !== value) backAttribs.set(key, oldValue);
}
// TODO: backAttribs does not restore removed attributes (it is missing attributes that
// are in oldAttribs but not in attribs). I don't know if that is intentional.
return backAttribs.toString();
});
let textLeftToProcess = textBank;
while (textLeftToProcess.length > 0) {
// process till the next line break or process only one line break
let lengthToProcess = textLeftToProcess.indexOf('\n');
let lineBreak = false;
switch (lengthToProcess) {
case -1:
lengthToProcess = textLeftToProcess.length;
break;
case 0:
lineBreak = true;
lengthToProcess = 1;
break;
}
// get the text we want to procceed in this step
const processText = textLeftToProcess.substr(0, lengthToProcess);
textLeftToProcess = textLeftToProcess.substr(lengthToProcess);
if (lineBreak) {
builder.keep(1, 1); // just skip linebreaks, don't do a insert + keep for a linebreak
// consume the attributes of this linebreak
consumeAttribRuns(1, () => {});
} else {
// add the old text via an insert, but add a deletion attribute +
// the author attribute of the author who deleted it
let textBankIndex = 0;
consumeAttribRuns(lengthToProcess, (len: number, attribs:string, endsLine: string) => {
// get the old attributes back
const oldAttribs = undoBackToAttribs(attribs);
builder.insert(processText.substr(textBankIndex, len), oldAttribs);
textBankIndex += len;
});
builder.keep(lengthToProcess, 0);
}
}
} else {
skip(csOp.chars, csOp.lines);
builder.keep(csOp.chars, csOp.lines);
}
} else if (csOp.opcode === '+') {
builder.keep(csOp.chars, csOp.lines);
} else if (csOp.opcode === '-') {
const textBank = nextText(csOp.chars);
let textBankIndex = 0;
consumeAttribRuns(csOp.chars, (len: number, attribs: string[], endsLine: string) => {
builder.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs);
textBankIndex += len;
});
}
}
return Changeset.checkRep(builder.toString());
}
}
// this method is 80% like Changeset.inverse. I just changed so instead of reverting,
// it adds deletions and attribute changes to to the atext.
PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
};
// export the constructor
module.exports = PadDiff;