mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-20 07:35:05 -04:00
Pad: Fix copyPadWithoutHistory
apool corruption bug
This commit is contained in:
parent
ed78b56079
commit
dab881139d
4 changed files with 96 additions and 5 deletions
|
@ -1,6 +1,8 @@
|
||||||
# 1.9.0 (not yet released)
|
# 1.9.0 (not yet released)
|
||||||
|
|
||||||
### Notable enhancements
|
### Notable enhancements and fixes
|
||||||
|
|
||||||
|
* Fixed a potential attribute pool corruption bug with `copyPadWithoutHistory`.
|
||||||
|
|
||||||
#### For plugin authors
|
#### For plugin authors
|
||||||
|
|
||||||
|
|
|
@ -492,10 +492,9 @@ 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');
|
||||||
|
newPad.pool = sourcePad.pool.clone();
|
||||||
|
|
||||||
const oldAText = this.atext;
|
const oldAText = this.atext;
|
||||||
const newPool = newPad.pool;
|
|
||||||
newPool.fromJsonable(sourcePad.pool.toJsonable()); // copy that sourceId pool to the new pad
|
|
||||||
|
|
||||||
// based on Changeset.makeSplice
|
// based on Changeset.makeSplice
|
||||||
const assem = Changeset.smartOpAssembler();
|
const assem = Changeset.smartOpAssembler();
|
||||||
|
|
|
@ -91,6 +91,17 @@ class AttributePool {
|
||||||
this.nextNum = 0;
|
this.nextNum = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {AttributePool} A deep copy of this attribute pool.
|
||||||
|
*/
|
||||||
|
clone() {
|
||||||
|
const c = new AttributePool();
|
||||||
|
for (const [n, a] of Object.entries(this.numToAttrib)) c.numToAttrib[n] = [a[0], a[1]];
|
||||||
|
Object.assign(c.attribToNum, this.attribToNum);
|
||||||
|
c.nextNum = this.nextNum;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an attribute to the attribute set, or query for an existing attribute identifier.
|
* Add an attribute to the attribute set, or query for an existing attribute identifier.
|
||||||
*
|
*
|
||||||
|
@ -164,7 +175,10 @@ class AttributePool {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Jsonable} An object that can be passed to `fromJsonable` to reconstruct this
|
* @returns {Jsonable} An object that can be passed to `fromJsonable` to reconstruct this
|
||||||
* attribute pool. The returned object can be converted to JSON.
|
* attribute pool. The returned object can be converted to JSON. WARNING: The returned object
|
||||||
|
* has references to internal state (it is not a deep copy). Use the `clone()` method to copy
|
||||||
|
* a pool -- do NOT do `new AttributePool().fromJsonable(pool.toJsonable())` to copy because
|
||||||
|
* the resulting shared state will lead to pool corruption.
|
||||||
*/
|
*/
|
||||||
toJsonable() {
|
toJsonable() {
|
||||||
return {
|
return {
|
||||||
|
@ -177,7 +191,10 @@ class AttributePool {
|
||||||
* Replace the contents of this attribute pool with values from a previous call to `toJsonable`.
|
* Replace the contents of this attribute pool with values from a previous call to `toJsonable`.
|
||||||
*
|
*
|
||||||
* @param {Jsonable} obj - Object returned by `toJsonable` containing the attributes and their
|
* @param {Jsonable} obj - Object returned by `toJsonable` containing the attributes and their
|
||||||
* identifiers.
|
* identifiers. WARNING: This function takes ownership of the object (it does not make a deep
|
||||||
|
* copy). Use the `clone()` method to copy a pool -- do NOT do
|
||||||
|
* `new AttributePool().fromJsonable(pool.toJsonable())` to copy because the resulting shared
|
||||||
|
* state will lead to pool corruption.
|
||||||
*/
|
*/
|
||||||
fromJsonable(obj) {
|
fromJsonable(obj) {
|
||||||
this.numToAttrib = obj.numToAttrib;
|
this.numToAttrib = obj.numToAttrib;
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
const assert = require('assert').strict;
|
const assert = require('assert').strict;
|
||||||
const common = require('../../common');
|
const common = require('../../common');
|
||||||
|
const padManager = require('../../../../node/db/PadManager');
|
||||||
|
|
||||||
let agent;
|
let agent;
|
||||||
const apiKey = common.apiKey;
|
const apiKey = common.apiKey;
|
||||||
|
@ -547,6 +548,78 @@ describe(__filename, function () {
|
||||||
assert.equal(res.body.code, 0);
|
assert.equal(res.body.code, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Regression test for https://github.com/ether/etherpad-lite/issues/5296
|
||||||
|
it('source and destination attribute pools are independent', async function () {
|
||||||
|
// Strategy for this test:
|
||||||
|
// 1. Create a new pad without bold or italic text
|
||||||
|
// 2. Use copyPadWithoutHistory to copy the pad.
|
||||||
|
// 3. Add some bold text (but no italic text!) to the source pad. This should add a bold
|
||||||
|
// attribute to the source pad's pool but not to the destination pad's pool.
|
||||||
|
// 4. Add some italic text (but no bold text!) to the destination pad. This should add an
|
||||||
|
// italic attribute to the destination pad's pool with the same number as the newly added
|
||||||
|
// bold attribute in the source pad's pool.
|
||||||
|
// 5. Add some more text (bold or plain) to the source pad. This will save the source pad to
|
||||||
|
// the database after the destination pad has had an opportunity to corrupt the source
|
||||||
|
// pad.
|
||||||
|
// 6. Export the source and destination pads. Make sure that <em> doesn't appear in the
|
||||||
|
// source pad's HTML, and that <strong> doesn't appear int he destination pad's HTML.
|
||||||
|
// 7. Force the server to re-init the pads from the database.
|
||||||
|
// 8. Repeat step 6.
|
||||||
|
// If <em> appears in the source pad, or <strong> appears in the destination pad, then shared
|
||||||
|
// state between the two attribute pools caused corruption.
|
||||||
|
|
||||||
|
const getHtml = async (padId) => {
|
||||||
|
const res = await agent.get(`${endPoint('getHTML')}&padID=${padId}`)
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /json/);
|
||||||
|
assert.equal(res.body.code, 0);
|
||||||
|
return res.body.data.html;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setBody = async (padId, bodyHtml) => {
|
||||||
|
await agent.post(endPoint('setHTML'))
|
||||||
|
.send({padID: padId, html: `<!DOCTYPE HTML><html><body>${bodyHtml}</body></html>`})
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect((res) => assert.equal(res.body.code, 0));
|
||||||
|
};
|
||||||
|
|
||||||
|
const origHtml = await getHtml(sourcePadId);
|
||||||
|
assert.doesNotMatch(origHtml, /<strong>/);
|
||||||
|
assert.doesNotMatch(origHtml, /<em>/);
|
||||||
|
await agent.get(`${endPoint('copyPadWithoutHistory')}&sourceID=${sourcePadId}` +
|
||||||
|
`&destinationID=${newPad}&force=false`)
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect((res) => assert.equal(res.body.code, 0));
|
||||||
|
|
||||||
|
const newBodySrc = '<strong>bold</strong>';
|
||||||
|
const newBodyDst = '<em>italic</em>';
|
||||||
|
await setBody(sourcePadId, newBodySrc);
|
||||||
|
await setBody(newPad, newBodyDst);
|
||||||
|
await setBody(sourcePadId, `${newBodySrc} foo`);
|
||||||
|
|
||||||
|
let [srcHtml, dstHtml] = await Promise.all([getHtml(sourcePadId), getHtml(newPad)]);
|
||||||
|
assert.match(srcHtml, new RegExp(newBodySrc));
|
||||||
|
assert.match(dstHtml, new RegExp(newBodyDst));
|
||||||
|
|
||||||
|
// Force the server to re-read the pads from the database. This rebuilds the attribute pool
|
||||||
|
// objects from scratch, ensuring that an internally inconsistent attribute pool object did
|
||||||
|
// not cause the above tests to accidentally pass.
|
||||||
|
const reInitPad = async (padId) => {
|
||||||
|
const pad = await padManager.getPad(padId);
|
||||||
|
await pad.init();
|
||||||
|
};
|
||||||
|
await Promise.all([
|
||||||
|
reInitPad(sourcePadId),
|
||||||
|
reInitPad(newPad),
|
||||||
|
]);
|
||||||
|
|
||||||
|
[srcHtml, dstHtml] = await Promise.all([getHtml(sourcePadId), getHtml(newPad)]);
|
||||||
|
assert.match(srcHtml, new RegExp(newBodySrc));
|
||||||
|
assert.match(dstHtml, new RegExp(newBodyDst));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue