diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f9d701c..a672d4503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)). diff --git a/doc/api/http_api.md b/doc/api/http_api.md index bf3be2755..765de1ecb 100644 --- a/doc/api/http_api.md +++ b/doc/api/http_api.md @@ -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
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":"Welcome to Etherpad!

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!

Get involved with Etherpad at http://etherpad.org
aw

","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. diff --git a/src/node/db/API.js b/src/node/db/API.js index 7fe0e5ca1..9b2ecadc7 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -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 diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js index 29ab1b598..4302048c4 100644 --- a/src/node/db/GroupManager.js +++ b/src/node/db/GroupManager.js @@ -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); diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 6bc6e5378..9807c2ceb 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -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; diff --git a/src/tests/backend/specs/api/restoreRevision.js b/src/tests/backend/specs/api/restoreRevision.js index 8dbe7e15a..98709ab9b 100644 --- a/src/tests/backend/specs/api/restoreRevision.js +++ b/src/tests/backend/specs/api/restoreRevision.js @@ -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), ''); + }); }); });