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.
* Fixed race conditions in the `setText`, `appendText`, and `restoreRevision`
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.
* 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)).

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: 1, message:"groupID does not exist", data: null}`
#### createGroupPad(groupID, padName [, text])
#### createGroupPad(groupID, padName, [text], [authorId])
* API >= 1
* `authorId` in API >= 1.3.0
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: 1, message:"padID does not exist", data: null}`
#### setText(padID, text)
#### setText(padID, text, [authorId])
* API >= 1
* `authorId` in API >= 1.3.0
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:"text too long", data: null}`
#### appendText(padID, text)
#### appendText(padID, text, [authorId])
* API >= 1.2.13
* `authorId` in API >= 1.3.0
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: 1, message:"padID does not exist", data: null}`
#### setHTML(padID, html)
#### setHTML(padID, html, [authorId])
* 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.
@ -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":4,"message":"no or wrong API Key","data":null}`
#### restoreRevision(padId, rev)
#### restoreRevision(padId, rev, [authorId])
* API >= 1.2.11
* `authorId` in API >= 1.3.0
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
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
* `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**.
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: 1, message:"padID does not exist", data: null}`
#### copyPadWithoutHistory(sourceID, destinationID[, force=false])
#### copyPadWithoutHistory(sourceID, destinationID, [force=false], [authorId])
* 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.
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:
@ -192,7 +192,7 @@ Example returns:
{code: 1, message:"padID does not exist", data: null}
{code: 1, message:"text too long", data: null}
*/
exports.setText = async (padID, text) => {
exports.setText = async (padID, text, authorId = '') => {
// text is required
if (typeof text !== 'string') {
throw new CustomError('text is not a string', 'apierror');
@ -201,12 +201,12 @@ exports.setText = async (padID, text) => {
// get the pad
const pad = await getPadSafe(padID, true);
await pad.setText(text);
await pad.setText(text, authorId);
await padMessageHandler.updatePadClients(pad);
};
/**
appendText(padID, text) appends text to a pad
appendText(padID, text, [authorId]) appends text to a pad
Example returns:
@ -214,14 +214,14 @@ Example returns:
{code: 1, message:"padID does not exist", data: null}
{code: 1, message:"text too long", data: null}
*/
exports.appendText = async (padID, text) => {
exports.appendText = async (padID, text, authorId = '') => {
// text is required
if (typeof text !== 'string') {
throw new CustomError('text is not a string', 'apierror');
}
const pad = await getPadSafe(padID, true);
await pad.appendText(text);
await pad.appendText(text, authorId);
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:
{code: 0, message:"ok", 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
if (typeof html !== 'string') {
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
try {
await importHtml.setPadHTML(pad, cleanText(html));
await importHtml.setPadHTML(pad, cleanText(html), authorId);
} catch (e) {
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:
{code: 0, message:"ok", data: null}
{code: 1, message:"pad does already exist", data: null}
*/
exports.createPad = async (padID, text) => {
exports.createPad = async (padID, text, authorId = '') => {
if (padID) {
// ensure there is no $ in the padID
if (padID.indexOf('$') !== -1) {
@ -480,7 +480,7 @@ exports.createPad = async (padID, text) => {
}
// 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:
{code:0, message:"ok", 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
if (rev === undefined) {
throw new CustomError('rev is not defined', 'apierror');
@ -555,7 +555,7 @@ exports.restoreRevision = async (padID, rev) => {
const changeset = builder.toString();
await pad.appendRevision(changeset);
await pad.appendRevision(changeset, authorId);
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,
the destination will be overwritten if it exists.
copyPadWithoutHistory(sourceID, destinationID[, force=false], [authorId]) copies a pad. If force is
true, the destination will be overwritten if it exists.
Example returns:
{code: 0, message:"ok", data: {padID: destinationID}}
{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);
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);
// gets a pad safe
const getPadSafe = async (padID, shouldExist, text) => {
const getPadSafe = async (padID, shouldExist, text, authorId = '') => {
// check if padID is a string
if (typeof padID !== 'string') {
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
return padManager.getPad(padID, text);
return padManager.getPad(padID, text, authorId);
};
// checks if a rev is a legal number

View File

@ -103,7 +103,7 @@ exports.createGroupIfNotExistsFor = async (groupMapper) => {
return result;
};
exports.createGroupPad = async (groupID, padName, text) => {
exports.createGroupPad = async (groupID, padName, text, authorId = '') => {
// create the padID
const padID = `${groupID}$${padName}`;
@ -123,7 +123,7 @@ exports.createGroupPad = async (groupID, padName, text) => {
}
// create the pad
await padManager.getPad(padID, text);
await padManager.getPad(padID, text, authorId);
// create an entry in the group for this pad
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']}
);
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
exports.latestApiVersion = '1.2.15';
exports.latestApiVersion = '1.3.0';
// exports the versions so it can be used by the new Swagger endpoint
exports.version = version;

View File

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