From a2e77a71288ea19f851d82eff9f079de0ba6d538 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 25 Nov 2021 18:14:38 -0500 Subject: [PATCH] ImportEtherpad: Enforce single-pad records --- src/node/utils/ImportEtherpad.js | 14 +++++++++ src/tests/backend/specs/ImportEtherpad.js | 38 +++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.js index cce4f5145..cfa6a2643 100644 --- a/src/node/utils/ImportEtherpad.js +++ b/src/node/utils/ImportEtherpad.js @@ -41,6 +41,12 @@ exports.setPadRaw = async (padId, r) => { 'pad', ]; + let originalPadId = null; + const checkOriginalPadId = (padId) => { + if (originalPadId == null) originalPadId = padId; + if (originalPadId !== padId) throw new Error('unexpected pad ID in record'); + }; + await Promise.all(Object.entries(records).map(async ([key, value]) => { if (!value) { return; @@ -48,12 +54,20 @@ exports.setPadRaw = async (padId, r) => { const keyParts = key.split(':'); const [prefix, id] = keyParts; if (prefix === 'globalAuthor' && keyParts.length === 2) { + // In the database, the padIDs subkey is an object (which is used as a set) that records every + // pad the author has worked on. When exported, that object becomes a single string containing + // the exported pad's ID. + if (typeof value.padIDs !== 'string') { + throw new TypeError('globalAuthor padIDs subkey is not a string'); + } + checkOriginalPadId(value.padIDs); if (await authorManager.doesAuthorExist(id)) { await authorManager.addPad(id, padId); return; } value.padIDs = {[padId]: 1}; } else if (padKeyPrefixes.includes(prefix)) { + checkOriginalPadId(id); if (prefix === 'pad' && keyParts.length === 2 && value.pool) { for (const [k] of Object.values(value.pool.numToAttrib)) { if (!supportedElems.has(k)) unsupportedElements.add(k); diff --git a/src/tests/backend/specs/ImportEtherpad.js b/src/tests/backend/specs/ImportEtherpad.js index a339e9b4d..7906c3afb 100644 --- a/src/tests/backend/specs/ImportEtherpad.js +++ b/src/tests/backend/specs/ImportEtherpad.js @@ -119,4 +119,42 @@ describe(__filename, function () { assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]); }); }); + + describe('enforces consistent pad ID', function () { + it('pad record has different pad ID', async function () { + const data = makeExport(makeAuthorId()); + data['pad:differentPadId'] = data['pad:testing']; + delete data['pad:testing']; + assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/); + }); + + it('globalAuthor record has different pad ID', async function () { + const authorId = makeAuthorId(); + const data = makeExport(authorId); + data[`globalAuthor:${authorId}`].padIDs = 'differentPadId'; + assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/); + }); + + it('pad rev record has different pad ID', async function () { + const data = makeExport(makeAuthorId()); + data['pad:differentPadId:revs:0'] = data['pad:testing:revs:0']; + delete data['pad:testing:revs:0']; + assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/); + }); + }); + + describe('order of records does not matter', function () { + for (const perm of [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]) { + it(JSON.stringify(perm), async function () { + const authorId = makeAuthorId(); + const records = Object.entries(makeExport(authorId)); + assert.equal(records.length, 3); + await importEtherpad.setPadRaw( + padId, JSON.stringify(Object.fromEntries(perm.map((i) => records[i])))); + assert.deepEqual((await authorManager.listPadsOfAuthor(authorId)).padIDs, [padId]); + const pad = await padManager.getPad(padId); + assert.equal(pad.text(), 'foo\n'); + }); + } + }); });