Moved more classes to ts. (#6179)
This commit is contained in:
parent
3ea6f1072d
commit
4bd27a1c79
22 changed files with 790 additions and 653 deletions
|
@ -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(
|
|
@ -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;
|
|
@ -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
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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);
|
||||||
};
|
};
|
|
@ -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
|
|
@ -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;
|
|
@ -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);
|
|
@ -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);
|
|
@ -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'});
|
||||||
});
|
});
|
|
@ -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');
|
|
@ -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('?'));
|
||||||
});
|
});
|
||||||
};
|
};
|
|
@ -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 = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
10
src/node/types/PartType.ts
Normal file
10
src/node/types/PartType.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export type PartType = {
|
||||||
|
plugin: string,
|
||||||
|
client_hooks:any
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginDef = {
|
||||||
|
package:{
|
||||||
|
path:string
|
||||||
|
}
|
||||||
|
}
|
5
src/node/types/UserSettingsObject.ts
Normal file
5
src/node/types/UserSettingsObject.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export type UserSettingsObject = {
|
||||||
|
canCreate: boolean,
|
||||||
|
readOnly: boolean,
|
||||||
|
padAuthorizations: any
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
|
@ -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)) {
|
|
@ -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.';
|
|
@ -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
458
src/node/utils/padDiff.ts
Normal 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;
|
Loading…
Reference in a new issue