Pad: Plumb author ID through mutation operations

This commit is contained in:
Richard Hansen 2022-02-17 00:01:07 -05:00
parent 5f60b3aab2
commit 3b8549342a
7 changed files with 35 additions and 30 deletions

View file

@ -274,8 +274,9 @@ Pad.prototype.text = function () {
* @param {number} ndel - Number of characters to remove starting at `start`. Must be a non-negative
* integer less than or equal to `this.text().length - start`.
* @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).
*/
Pad.prototype.spliceText = async function (start, ndel, ins) {
Pad.prototype.spliceText = async function (start, ndel, ins, authorId = '') {
if (start < 0) throw new RangeError(`start index must be non-negative (is ${start})`);
if (ndel < 0) throw new RangeError(`characters to delete must be non-negative (is ${ndel})`);
const orig = this.text();
@ -289,7 +290,7 @@ Pad.prototype.spliceText = async function (start, ndel, ins) {
if (!willEndWithNewline) ins += '\n';
if (ndel === 0 && ins.length === 0) return;
const changeset = Changeset.makeSplice(orig, start, ndel, ins);
await this.appendRevision(changeset);
await this.appendRevision(changeset, authorId);
};
/**
@ -297,18 +298,20 @@ Pad.prototype.spliceText = async function (start, ndel, ins) {
*
* @param {string} newText - The pad's new text. If this string does not end with a newline, one
* will be automatically appended.
* @param {string} [authorId] - The author ID of the user that initiated the change, if applicable.
*/
Pad.prototype.setText = async function (newText) {
await this.spliceText(0, this.text().length, newText);
Pad.prototype.setText = async function (newText, authorId = '') {
await this.spliceText(0, this.text().length, newText, authorId);
};
/**
* Appends text to the pad.
*
* @param {string} newText - Text to insert just BEFORE the pad's existing terminating newline.
* @param {string} [authorId] - The author ID of the user that initiated the change, if applicable.
*/
Pad.prototype.appendText = async function (newText) {
await this.spliceText(this.text().length - 1, 0, newText);
Pad.prototype.appendText = async function (newText, authorId = '') {
await this.spliceText(this.text().length - 1, 0, newText, authorId);
};
/**
@ -368,7 +371,7 @@ Pad.prototype.getChatMessages = async function (start, end) {
});
};
Pad.prototype.init = async function (text) {
Pad.prototype.init = async function (text, authorId = '') {
// replace text with default text if text isn't set
if (text == null) {
text = settings.defaultPadText;
@ -391,7 +394,7 @@ Pad.prototype.init = async function (text) {
// this pad doesn't exist, so create it
const firstChangeset = Changeset.makeSplice('\n', 0, 0, exports.cleanText(text));
await this.appendRevision(firstChangeset, '');
await this.appendRevision(firstChangeset, authorId);
}
};
@ -476,7 +479,7 @@ Pad.prototype.copyAuthorInfoToDestinationPad = async function (destinationID) {
(authorID) => authorManager.addPad(authorID, destinationID)));
};
Pad.prototype.copyPadWithoutHistory = async function (destinationID, force) {
Pad.prototype.copyPadWithoutHistory = async function (destinationID, force, authorId = '') {
// flush the source pad
this.saveToDatabase();
@ -494,7 +497,7 @@ Pad.prototype.copyPadWithoutHistory = async function (destinationID, force) {
}
// initialize the pad with a new line to avoid getting the defaultText
const newPad = await padManager.getPad(destinationID, '\n');
const newPad = await padManager.getPad(destinationID, '\n', authorId);
newPad.pool = this.pool.clone();
const oldAText = this.atext;
@ -514,7 +517,7 @@ Pad.prototype.copyPadWithoutHistory = async function (destinationID, force) {
// create a changeset that removes the previous text and add the newText with
// all atributes present on the source pad
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
newPad.appendRevision(changeset);
newPad.appendRevision(changeset, authorId);
await hooks.aCallAll('padCopy', {originalPad: this, destinationID});

View file

@ -91,9 +91,11 @@ const padList = new class {
/**
* Returns a Pad Object with the callback
* @param id A String with the id of the pad
* @param {Function} callback
* @param {string} [text] - Optional initial pad text if creating a new pad.
* @param {string} [authorId] - Optional author ID of the user that initiated the pad creation (if
* applicable).
*/
exports.getPad = async (id, text) => {
exports.getPad = async (id, text, authorId = '') => {
// check if this is a valid padId
if (!exports.isValidPadId(id)) {
throw new CustomError(`${id} is not a valid padId`, 'apierror');
@ -123,7 +125,7 @@ exports.getPad = async (id, text) => {
pad = new Pad.Pad(id);
// initialize the pad
await pad.init(text);
await pad.init(text, authorId);
hooks.callAll('padLoad', {pad});
globalPads.set(id, pad);
padList.addPad(id);

View file

@ -74,7 +74,7 @@ const tmpDirectory = os.tmpdir();
/**
* do a requested import
*/
const doImport = async (req, res, padId) => {
const doImport = async (req, res, padId, authorId) => {
// pipe to a file
// convert file to html via abiword or soffice
// set html in the pad
@ -140,7 +140,7 @@ const doImport = async (req, res, padId) => {
let directDatabaseAccess = false;
if (fileIsEtherpad) {
// Use '\n' to avoid the default pad text if the pad doesn't yet exist.
const pad = await padManager.getPad(padId, '\n');
const pad = await padManager.getPad(padId, '\n', authorId);
const headCount = pad.head;
if (headCount >= 10) {
logger.warn('Aborting direct database import attempt of a pad that already has content');
@ -148,7 +148,7 @@ const doImport = async (req, res, padId) => {
}
const text = await fs.readFile(srcFile, 'utf8');
directDatabaseAccess = true;
await importEtherpad.setPadRaw(padId, text);
await importEtherpad.setPadRaw(padId, text, authorId);
}
// convert file to html if necessary
@ -205,12 +205,12 @@ const doImport = async (req, res, padId) => {
if (!directDatabaseAccess) {
if (importHandledByPlugin || useConverter || fileIsHTML) {
try {
await importHtml.setPadHTML(pad, text);
await importHtml.setPadHTML(pad, text, authorId);
} catch (err) {
logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`);
}
} else {
await pad.setText(text);
await pad.setText(text, authorId);
}
}
@ -233,13 +233,13 @@ const doImport = async (req, res, padId) => {
return false;
};
exports.doImport = async (req, res, padId) => {
exports.doImport = async (req, res, padId, authorId = '') => {
let httpStatus = 200;
let code = 0;
let message = 'ok';
let directDatabaseAccess;
try {
directDatabaseAccess = await doImport(req, res, padId);
directDatabaseAccess = await doImport(req, res, padId, authorId);
} catch (err) {
const known = err instanceof ImportError && err.status;
if (!known) logger.error(`Internal error during import: ${err.stack || err}`);

View file

@ -676,13 +676,13 @@ const handleUserChanges = async (socket, message) => {
const correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool);
if (correctionChangeset) {
await pad.appendRevision(correctionChangeset);
await pad.appendRevision(correctionChangeset, thisSession.author);
}
// Make sure the pad always ends with an empty line.
if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) {
const nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, '\n');
await pad.appendRevision(nlChangeset);
await pad.appendRevision(nlChangeset, thisSession.author);
}
// The client assumes that ACCEPT_COMMIT and NEW_CHANGES messages arrive in order. Make sure we

View file

@ -70,12 +70,12 @@ exports.expressCreateServer = (hookName, args, cb) => {
args.app.post('/p/:pad/import', (req, res, next) => {
(async () => {
const {session: {user} = {}} = req;
const {accessStatus} = await securityManager.checkAccess(
const {accessStatus, authorID: authorId} = await securityManager.checkAccess(
req.params.pad, req.cookies.sessionID, req.cookies.token, user);
if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) {
return res.status(403).send('Forbidden');
}
await importHandler.doImport(req, res, req.params.pad);
await importHandler.doImport(req, res, req.params.pad, authorId);
})().catch((err) => next(err || new Error(err)));
});

View file

@ -27,7 +27,7 @@ const supportedElems = require('../../static/js/contentcollector').supportedElem
const logger = log4js.getLogger('ImportEtherpad');
exports.setPadRaw = async (padId, r) => {
exports.setPadRaw = async (padId, r, authorId = '') => {
const records = JSON.parse(r);
// get supported block Elements from plugins, we will use this later.
@ -110,7 +110,7 @@ exports.setPadRaw = async (padId, r) => {
return v;
},
});
await pad.init();
await pad.init(null, authorId);
await pad.check();
await Promise.all([

View file

@ -23,7 +23,7 @@ const jsdom = require('jsdom');
const apiLogger = log4js.getLogger('ImportHtml');
let processor;
exports.setPadHTML = async (pad, html) => {
exports.setPadHTML = async (pad, html, authorId = '') => {
if (processor == null) {
const [{rehype}, {default: minifyWhitespace}] =
await Promise.all([import('rehype'), import('rehype-minify-whitespace')]);
@ -88,6 +88,6 @@ exports.setPadHTML = async (pad, html) => {
const theChangeset = builder.toString();
apiLogger.debug(`The changeset: ${theChangeset}`);
await pad.setText('\n');
await pad.appendRevision(theChangeset);
await pad.setText('\n', authorId);
await pad.appendRevision(theChangeset, authorId);
};