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 * @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`. * 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} 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 (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();
@ -289,7 +290,7 @@ Pad.prototype.spliceText = async function (start, ndel, ins) {
if (!willEndWithNewline) ins += '\n'; if (!willEndWithNewline) ins += '\n';
if (ndel === 0 && ins.length === 0) return; if (ndel === 0 && ins.length === 0) return;
const changeset = Changeset.makeSplice(orig, start, ndel, ins); 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 * @param {string} newText - The pad's new text. If this string does not end with a newline, one
* will be automatically appended. * 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) { Pad.prototype.setText = async function (newText, authorId = '') {
await this.spliceText(0, this.text().length, newText); await this.spliceText(0, this.text().length, newText, authorId);
}; };
/** /**
* Appends text to the pad. * Appends text to the pad.
* *
* @param {string} newText - Text to insert just BEFORE the pad's existing terminating newline. * @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) { Pad.prototype.appendText = async function (newText, authorId = '') {
await this.spliceText(this.text().length - 1, 0, newText); 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 // replace text with default text if text isn't set
if (text == null) { if (text == null) {
text = settings.defaultPadText; text = settings.defaultPadText;
@ -391,7 +394,7 @@ Pad.prototype.init = async function (text) {
// this pad doesn't exist, so create it // this pad doesn't exist, so create it
const firstChangeset = Changeset.makeSplice('\n', 0, 0, exports.cleanText(text)); 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))); (authorID) => authorManager.addPad(authorID, destinationID)));
}; };
Pad.prototype.copyPadWithoutHistory = async function (destinationID, force) { Pad.prototype.copyPadWithoutHistory = async function (destinationID, force, authorId = '') {
// flush the source pad // flush the source pad
this.saveToDatabase(); 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 // 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(); newPad.pool = this.pool.clone();
const oldAText = this.atext; 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 // create a changeset that removes the previous text and add the newText with
// all atributes present on the source pad // all atributes present on the source pad
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText); const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
newPad.appendRevision(changeset); newPad.appendRevision(changeset, authorId);
await hooks.aCallAll('padCopy', {originalPad: this, destinationID}); await hooks.aCallAll('padCopy', {originalPad: this, destinationID});

View file

@ -91,9 +91,11 @@ const padList = new class {
/** /**
* Returns a Pad Object with the callback * Returns a Pad Object with the callback
* @param id A String with the id of the pad * @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 // 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');
@ -123,7 +125,7 @@ exports.getPad = async (id, text) => {
pad = new Pad.Pad(id); pad = new Pad.Pad(id);
// initialize the pad // initialize the pad
await pad.init(text); await pad.init(text, authorId);
hooks.callAll('padLoad', {pad}); hooks.callAll('padLoad', {pad});
globalPads.set(id, pad); globalPads.set(id, pad);
padList.addPad(id); padList.addPad(id);

View file

@ -74,7 +74,7 @@ const tmpDirectory = os.tmpdir();
/** /**
* do a requested import * do a requested import
*/ */
const doImport = async (req, res, padId) => { const doImport = async (req, res, padId, authorId) => {
// pipe to a file // pipe to a file
// convert file to html via abiword or soffice // convert file to html via abiword or soffice
// set html in the pad // set html in the pad
@ -140,7 +140,7 @@ const doImport = async (req, res, padId) => {
let directDatabaseAccess = false; let directDatabaseAccess = false;
if (fileIsEtherpad) { if (fileIsEtherpad) {
// Use '\n' to avoid the default pad text if the pad doesn't yet exist. // 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; const headCount = pad.head;
if (headCount >= 10) { if (headCount >= 10) {
logger.warn('Aborting direct database import attempt of a pad that already has content'); 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'); const text = await fs.readFile(srcFile, 'utf8');
directDatabaseAccess = true; directDatabaseAccess = true;
await importEtherpad.setPadRaw(padId, text); await importEtherpad.setPadRaw(padId, text, authorId);
} }
// convert file to html if necessary // convert file to html if necessary
@ -205,12 +205,12 @@ const doImport = async (req, res, padId) => {
if (!directDatabaseAccess) { if (!directDatabaseAccess) {
if (importHandledByPlugin || useConverter || fileIsHTML) { if (importHandledByPlugin || useConverter || fileIsHTML) {
try { try {
await importHtml.setPadHTML(pad, text); await importHtml.setPadHTML(pad, text, authorId);
} catch (err) { } catch (err) {
logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`); logger.warn(`Error importing, possibly caused by malformed HTML: ${err.stack || err}`);
} }
} else { } else {
await pad.setText(text); await pad.setText(text, authorId);
} }
} }
@ -233,13 +233,13 @@ const doImport = async (req, res, padId) => {
return false; return false;
}; };
exports.doImport = async (req, res, padId) => { exports.doImport = async (req, res, padId, authorId = '') => {
let httpStatus = 200; let httpStatus = 200;
let code = 0; let code = 0;
let message = 'ok'; let message = 'ok';
let directDatabaseAccess; let directDatabaseAccess;
try { try {
directDatabaseAccess = await doImport(req, res, padId); directDatabaseAccess = await doImport(req, res, padId, authorId);
} catch (err) { } catch (err) {
const known = err instanceof ImportError && err.status; const known = err instanceof ImportError && err.status;
if (!known) logger.error(`Internal error during import: ${err.stack || err}`); 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); const correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool);
if (correctionChangeset) { if (correctionChangeset) {
await pad.appendRevision(correctionChangeset); await pad.appendRevision(correctionChangeset, thisSession.author);
} }
// Make sure the pad always ends with an empty line. // Make sure the pad always ends with an empty line.
if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) { if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) {
const nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, '\n'); 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 // 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) => { args.app.post('/p/:pad/import', (req, res, next) => {
(async () => { (async () => {
const {session: {user} = {}} = req; 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); req.params.pad, req.cookies.sessionID, req.cookies.token, user);
if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) { if (accessStatus !== 'grant' || !webaccess.userCanModify(req.params.pad, req)) {
return res.status(403).send('Forbidden'); 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))); })().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'); const logger = log4js.getLogger('ImportEtherpad');
exports.setPadRaw = async (padId, r) => { exports.setPadRaw = async (padId, r, authorId = '') => {
const records = JSON.parse(r); const records = JSON.parse(r);
// get supported block Elements from plugins, we will use this later. // get supported block Elements from plugins, we will use this later.
@ -110,7 +110,7 @@ exports.setPadRaw = async (padId, r) => {
return v; return v;
}, },
}); });
await pad.init(); await pad.init(null, authorId);
await pad.check(); await pad.check();
await Promise.all([ await Promise.all([

View file

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