API: Add optional authorId param to mutation functions

This commit is contained in:
Richard Hansen 2022-02-16 23:25:19 -05:00
parent 50fafe608b
commit aa286b7dbd
6 changed files with 93 additions and 42 deletions

View file

@ -26,6 +26,9 @@
database when the group is deleted. database when the group is deleted.
* Fixed race conditions in the `setText`, `appendText`, and `restoreRevision` * Fixed race conditions in the `setText`, `appendText`, and `restoreRevision`
functions. functions.
* Added an optional `authorId` parameter to `appendText`,
`copyPadWithoutHistory`, `createGroupPad`, `createPad`, `restoreRevision`,
`setHTML`, and `setText`, and bumped the latest API version to 1.3.0.
* Fixed a crash if the database is busy enough to cause a query timeout. * Fixed a crash if the database is busy enough to cause a query timeout.
* New `/health` endpoint for getting information about Etherpad's health (see * New `/health` endpoint for getting information about Etherpad's health (see
[draft-inadarei-api-health-check-06](https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html)). [draft-inadarei-api-health-check-06](https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html)).

View file

@ -173,8 +173,9 @@ returns all pads of this group
* `{code: 0, message:"ok", data: {padIDs : ["g.s8oes9dhwrvt0zif$test", "g.s8oes9dhwrvt0zif$test2"]}` * `{code: 0, message:"ok", data: {padIDs : ["g.s8oes9dhwrvt0zif$test", "g.s8oes9dhwrvt0zif$test2"]}`
* `{code: 1, message:"groupID does not exist", data: null}` * `{code: 1, message:"groupID does not exist", data: null}`
#### createGroupPad(groupID, padName [, text]) #### createGroupPad(groupID, padName, [text], [authorId])
* API >= 1 * API >= 1
* `authorId` in API >= 1.3.0
creates a new pad in this group creates a new pad in this group
@ -293,8 +294,9 @@ returns the text of a pad
* `{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}`
#### setText(padID, text) #### setText(padID, text, [authorId])
* API >= 1 * API >= 1
* `authorId` in API >= 1.3.0
Sets the text of a pad. Sets the text of a pad.
@ -305,8 +307,9 @@ If your text is long (>8 KB), please invoke via POST and include `text` paramete
* `{code: 1, message:"padID does not exist", data: null}` * `{code: 1, message:"padID does not exist", data: null}`
* `{code: 1, message:"text too long", data: null}` * `{code: 1, message:"text too long", data: null}`
#### appendText(padID, text) #### appendText(padID, text, [authorId])
* API >= 1.2.13 * API >= 1.2.13
* `authorId` in API >= 1.3.0
Appends text to a pad. Appends text to a pad.
@ -326,8 +329,9 @@ returns the text of a pad formatted as HTML
* `{code: 0, message:"ok", data: {html:"Welcome Text<br>More Text"}}` * `{code: 0, message:"ok", data: {html:"Welcome Text<br>More Text"}}`
* `{code: 1, message:"padID does not exist", data: null}` * `{code: 1, message:"padID does not exist", data: null}`
#### setHTML(padID, html) #### setHTML(padID, html, [authorId])
* API >= 1 * API >= 1
* `authorId` in API >= 1.3.0
sets the text of a pad based on HTML, HTML must be well-formed. Malformed HTML will send a warning to the API log. sets the text of a pad based on HTML, HTML must be well-formed. Malformed HTML will send a warning to the API log.
@ -387,8 +391,9 @@ returns an object of diffs from 2 points in a pad
* `{"code":0,"message":"ok","data":{"html":"<style>\n.authora_HKIv23mEbachFYfH {background-color: #a979d9}\n.authora_n4gEeMLsv1GivNeh {background-color: #a9b5d9}\n.removed {text-decoration: line-through; -ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=80)'; filter: alpha(opacity=80); opacity: 0.8; }\n</style>Welcome to Etherpad!<br><br>This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!<br><br>Get involved with Etherpad at <a href=\"http&#x3a;&#x2F;&#x2F;etherpad&#x2e;org\">http:&#x2F;&#x2F;etherpad.org</a><br><span class=\"authora_HKIv23mEbachFYfH\">aw</span><br><br>","authors":["a.HKIv23mEbachFYfH",""]}}` * `{"code":0,"message":"ok","data":{"html":"<style>\n.authora_HKIv23mEbachFYfH {background-color: #a979d9}\n.authora_n4gEeMLsv1GivNeh {background-color: #a9b5d9}\n.removed {text-decoration: line-through; -ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=80)'; filter: alpha(opacity=80); opacity: 0.8; }\n</style>Welcome to Etherpad!<br><br>This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!<br><br>Get involved with Etherpad at <a href=\"http&#x3a;&#x2F;&#x2F;etherpad&#x2e;org\">http:&#x2F;&#x2F;etherpad.org</a><br><span class=\"authora_HKIv23mEbachFYfH\">aw</span><br><br>","authors":["a.HKIv23mEbachFYfH",""]}}`
* `{"code":4,"message":"no or wrong API Key","data":null}` * `{"code":4,"message":"no or wrong API Key","data":null}`
#### restoreRevision(padId, rev) #### restoreRevision(padId, rev, [authorId])
* API >= 1.2.11 * API >= 1.2.11
* `authorId` in API >= 1.3.0
Restores revision from past as new changeset Restores revision from past as new changeset
@ -437,8 +442,9 @@ creates a chat message, saves it to the database and sends it to all connected c
### Pad ### Pad
Group pads are normal pads, but with the name schema GROUPID$PADNAME. A security manager controls access of them and it's forbidden for normal pads to include a $ in the name. Group pads are normal pads, but with the name schema GROUPID$PADNAME. A security manager controls access of them and it's forbidden for normal pads to include a $ in the name.
#### createPad(padID [, text]) #### createPad(padID, [text], [authorId])
* API >= 1 * API >= 1
* `authorId` in API >= 1.3.0
creates a new (non-group) pad. Note that if you need to create a group Pad, you should call **createGroupPad**. creates a new (non-group) pad. Note that if you need to create a group Pad, you should call **createGroupPad**.
You get an error message if you use one of the following characters in the padID: "/", "?", "&" or "#". You get an error message if you use one of the following characters in the padID: "/", "?", "&" or "#".
@ -519,8 +525,9 @@ copies a pad with full history and chat. If force is true and the destination pa
* `{code: 0, message:"ok", data: null}` * `{code: 0, message:"ok", data: null}`
* `{code: 1, message:"padID does not exist", data: null}` * `{code: 1, message:"padID does not exist", data: null}`
#### copyPadWithoutHistory(sourceID, destinationID[, force=false]) #### copyPadWithoutHistory(sourceID, destinationID, [force=false], [authorId])
* API >= 1.2.15 * API >= 1.2.15
* `authorId` in API >= 1.3.0
copies a pad without copying the history and chat. If force is true and the destination pad exists, it will be overwritten. copies a pad without copying the history and chat. If force is true and the destination pad exists, it will be overwritten.
Note that all the revisions will be lost! In most of the cases one should use `copyPad` API instead. Note that all the revisions will be lost! In most of the cases one should use `copyPad` API instead.

View file

@ -184,7 +184,7 @@ exports.getText = async (padID, rev) => {
}; };
/** /**
setText(padID, text) sets the text of a pad setText(padID, text, [authorId]) sets the text of a pad
Example returns: Example returns:
@ -192,7 +192,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
{code: 1, message:"text too long", data: null} {code: 1, message:"text too long", data: null}
*/ */
exports.setText = async (padID, text) => { exports.setText = async (padID, text, authorId = '') => {
// 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');
@ -201,12 +201,12 @@ exports.setText = async (padID, text) => {
// get the pad // get the pad
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
await pad.setText(text); await pad.setText(text, authorId);
await padMessageHandler.updatePadClients(pad); await padMessageHandler.updatePadClients(pad);
}; };
/** /**
appendText(padID, text) appends text to a pad appendText(padID, text, [authorId]) appends text to a pad
Example returns: Example returns:
@ -214,14 +214,14 @@ Example returns:
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
{code: 1, message:"text too long", data: null} {code: 1, message:"text too long", data: null}
*/ */
exports.appendText = async (padID, text) => { exports.appendText = async (padID, text, authorId = '') => {
// 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');
} }
const pad = await getPadSafe(padID, true); const pad = await getPadSafe(padID, true);
await pad.appendText(text); await pad.appendText(text, authorId);
await padMessageHandler.updatePadClients(pad); await padMessageHandler.updatePadClients(pad);
}; };
@ -258,14 +258,14 @@ exports.getHTML = async (padID, rev) => {
}; };
/** /**
setHTML(padID, html) sets the text of a pad based on HTML setHTML(padID, html, [authorId]) sets the text of a pad based on HTML
Example returns: Example returns:
{code: 0, message:"ok", data: null} {code: 0, message:"ok", data: null}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.setHTML = async (padID, html) => { exports.setHTML = async (padID, html, 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');
@ -276,7 +276,7 @@ exports.setHTML = async (padID, html) => {
// add a new changeset with the new html to the pad // add a new changeset with the new html to the pad
try { try {
await importHtml.setPadHTML(pad, cleanText(html)); await importHtml.setPadHTML(pad, cleanText(html), authorId);
} catch (e) { } catch (e) {
throw new CustomError('HTML is malformed', 'apierror'); throw new CustomError('HTML is malformed', 'apierror');
} }
@ -459,14 +459,14 @@ exports.getLastEdited = async (padID) => {
}; };
/** /**
createPad(padName [, text]) creates a new pad in this group createPad(padName, [text], [authorId]) creates a new pad in this group
Example returns: 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}
*/ */
exports.createPad = async (padID, text) => { exports.createPad = async (padID, text, 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) {
@ -480,7 +480,7 @@ exports.createPad = async (padID, text) => {
} }
// create pad // create pad
await getPadSafe(padID, false, text); await getPadSafe(padID, false, text, authorId);
}; };
/** /**
@ -497,14 +497,14 @@ exports.deletePad = async (padID) => {
}; };
/** /**
restoreRevision(padID, [rev]) Restores revision from past as new changeset restoreRevision(padID, rev, [authorId]) Restores revision from past as new changeset
Example returns: Example returns:
{code:0, message:"ok", data:null} {code:0, message:"ok", data:null}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.restoreRevision = async (padID, rev) => { exports.restoreRevision = async (padID, rev, 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');
@ -555,7 +555,7 @@ exports.restoreRevision = async (padID, rev) => {
const changeset = builder.toString(); const changeset = builder.toString();
await pad.appendRevision(changeset); await pad.appendRevision(changeset, authorId);
await padMessageHandler.updatePadClients(pad); await padMessageHandler.updatePadClients(pad);
}; };
@ -574,17 +574,17 @@ exports.copyPad = async (sourceID, destinationID, force) => {
}; };
/** /**
copyPadWithoutHistory(sourceID, destinationID[, force=false]) copies a pad. If force is true, copyPadWithoutHistory(sourceID, destinationID[, force=false], [authorId]) copies a pad. If force is
the destination will be overwritten if it exists. true, the destination will be overwritten if it exists.
Example returns: Example returns:
{code: 0, message:"ok", data: {padID: destinationID}} {code: 0, message:"ok", data: {padID: destinationID}}
{code: 1, message:"padID does not exist", data: null} {code: 1, message:"padID does not exist", data: null}
*/ */
exports.copyPadWithoutHistory = async (sourceID, destinationID, force) => { exports.copyPadWithoutHistory = async (sourceID, destinationID, force, authorId = '') => {
const pad = await getPadSafe(sourceID, true); const pad = await getPadSafe(sourceID, true);
await pad.copyPadWithoutHistory(destinationID, force); await pad.copyPadWithoutHistory(destinationID, force, authorId);
}; };
/** /**
@ -826,7 +826,7 @@ exports.getStats = async () => {
const isInt = (value) => (parseFloat(value) === parseInt(value, 10)) && !isNaN(value); const isInt = (value) => (parseFloat(value) === parseInt(value, 10)) && !isNaN(value);
// gets a pad safe // gets a pad safe
const getPadSafe = async (padID, shouldExist, text) => { const getPadSafe = async (padID, shouldExist, text, authorId = '') => {
// 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');
@ -851,7 +851,7 @@ const getPadSafe = async (padID, shouldExist, text) => {
} }
// pad exists, let's get it // pad exists, let's get it
return padManager.getPad(padID, text); return padManager.getPad(padID, text, authorId);
}; };
// checks if a rev is a legal number // checks if a rev is a legal number

View file

@ -103,7 +103,7 @@ exports.createGroupIfNotExistsFor = async (groupMapper) => {
return result; return result;
}; };
exports.createGroupPad = async (groupID, padName, text) => { exports.createGroupPad = async (groupID, padName, text, authorId = '') => {
// create the padID // create the padID
const padID = `${groupID}$${padName}`; const padID = `${groupID}$${padName}`;
@ -123,7 +123,7 @@ exports.createGroupPad = async (groupID, padName, text) => {
} }
// create the pad // create the pad
await padManager.getPad(padID, text); await padManager.getPad(padID, text, authorId);
// create an entry in the group for this pad // create an entry in the group for this pad
await db.setSub(`group:${groupID}`, ['pads', padID], 1); await db.setSub(`group:${groupID}`, ['pads', padID], 1);

View file

@ -134,8 +134,19 @@ version['1.2.15'] = Object.assign({}, version['1.2.14'],
{copyPadWithoutHistory: ['sourceID', 'destinationID', 'force']} {copyPadWithoutHistory: ['sourceID', 'destinationID', 'force']}
); );
version['1.3.0'] = {
...version['1.2.15'],
appendText: ['padID', 'text', 'authorId'],
copyPadWithoutHistory: ['sourceID', 'destinationID', 'force', 'authorId'],
createGroupPad: ['groupID', 'padName', 'text', 'authorId'],
createPad: ['padID', 'text', 'authorId'],
restoreRevision: ['padID', 'rev', 'authorId'],
setHTML: ['padID', 'html', 'authorId'],
setText: ['padID', 'text', 'authorId'],
};
// set the latest available API version here // set the latest available API version here
exports.latestApiVersion = '1.2.15'; exports.latestApiVersion = '1.3.0';
// exports the versions so it can be used by the new Swagger endpoint // exports the versions so it can be used by the new Swagger endpoint
exports.version = version; exports.version = version;

View file

@ -1,21 +1,24 @@
'use strict'; 'use strict';
const assert = require('assert').strict; const assert = require('assert').strict;
const authorManager = require('../../../../node/db/AuthorManager');
const common = require('../../common'); const common = require('../../common');
const padManager = require('../../../../node/db/PadManager'); const padManager = require('../../../../node/db/PadManager');
describe(__filename, function () { describe(__filename, function () {
let agent; let agent;
let authorId;
let padId; let padId;
let pad; let pad;
const restoreRevision = async (padId, rev) => { const restoreRevision = async (v, padId, rev, authorId = null) => {
const p = new URLSearchParams(Object.entries({ const p = new URLSearchParams(Object.entries({
apikey: common.apiKey, apikey: common.apiKey,
padID: padId, padID: padId,
rev, rev,
...(authorId == null ? {} : {authorId}),
})); }));
const res = await agent.get(`/api/1.2.11/restoreRevision?${p}`) const res = await agent.get(`/api/${v}/restoreRevision?${p}`)
.expect(200) .expect(200)
.expect('Content-Type', /json/); .expect('Content-Type', /json/);
assert.equal(res.body.code, 0); assert.equal(res.body.code, 0);
@ -23,6 +26,8 @@ describe(__filename, function () {
before(async function () { before(async function () {
agent = await common.init(); agent = await common.init();
authorId = await authorManager.getAuthor4Token('test-restoreRevision');
assert(authorId);
}); });
beforeEach(async function () { beforeEach(async function () {
@ -38,14 +43,39 @@ describe(__filename, function () {
if (await padManager.doesPadExist(padId)) await padManager.removePad(padId); if (await padManager.doesPadExist(padId)) await padManager.removePad(padId);
}); });
describe('v1.2.11', function () {
// TODO: Enable once the end-of-pad newline bugs are fixed. See: // TODO: Enable once the end-of-pad newline bugs are fixed. See:
// https://github.com/ether/etherpad-lite/pull/5253 // https://github.com/ether/etherpad-lite/pull/5253
xit('content matches', async function () { xit('content matches', async function () {
const oldHead = pad.head; const oldHead = pad.head;
const wantAText = await pad.getInternalRevisionAText(pad.head - 1); const wantAText = await pad.getInternalRevisionAText(pad.head - 1);
assert(wantAText.text.endsWith('\nfoo\n')); assert(wantAText.text.endsWith('\nfoo\n'));
await restoreRevision(padId, pad.head - 1); await restoreRevision('1.2.11', padId, pad.head - 1);
assert.equal(pad.head, oldHead + 1); assert.equal(pad.head, oldHead + 1);
assert.deepEqual(await pad.getInternalRevisionAText(pad.head), wantAText); assert.deepEqual(await pad.getInternalRevisionAText(pad.head), wantAText);
}); });
it('authorId ignored', async function () {
const oldHead = pad.head;
await restoreRevision('1.2.11', padId, pad.head - 1, authorId);
assert.equal(pad.head, oldHead + 1);
assert.equal(await pad.getRevisionAuthor(pad.head), '');
});
});
describe('v1.3.0', function () {
it('change is attributed to given authorId', async function () {
const oldHead = pad.head;
await restoreRevision('1.3.0', padId, pad.head - 1, authorId);
assert.equal(pad.head, oldHead + 1);
assert.equal(await pad.getRevisionAuthor(pad.head), authorId);
});
it('authorId can be omitted', async function () {
const oldHead = pad.head;
await restoreRevision('1.3.0', padId, pad.head - 1);
assert.equal(pad.head, oldHead + 1);
assert.equal(await pad.getRevisionAuthor(pad.head), '');
});
});
}); });