diff --git a/doc/api/http_api.md b/doc/api/http_api.md
index 5be9cb3a..7cdadefc 100644
--- a/doc/api/http_api.md
+++ b/doc/api/http_api.md
@@ -526,6 +526,16 @@ 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])
+* API >= 1.2.15
+
+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.
+
+*Example returns:*
+* `{code: 0, message:"ok", data: null}`
+* `{code: 1, message:"padID does not exist", data: null}`
+
#### movePad(sourceID, destinationID[, force=false])
* API >= 1.2.8
diff --git a/src/node/db/API.js b/src/node/db/API.js
index 71bd09ec..20946111 100644
--- a/src/node/db/API.js
+++ b/src/node/db/API.js
@@ -597,6 +597,21 @@ exports.copyPad = async function(sourceID, destinationID, force)
await pad.copy(destinationID, force);
}
+/**
+copyPadWithoutHistory(sourceID, destinationID[, force=false]) 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 function(sourceID, destinationID, force)
+{
+ let pad = await getPadSafe(sourceID, true);
+ await pad.copyPadWithoutHistory(destinationID, force);
+}
+
/**
movePad(sourceID, destinationID[, force=false]) moves a pad. If force is true,
the destination will be overwritten if it exists.
diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js
index 3fe1dda2..33cc38bc 100644
--- a/src/node/db/Pad.js
+++ b/src/node/db/Pad.js
@@ -357,16 +357,9 @@ Pad.prototype.init = async function init(text) {
}
Pad.prototype.copy = async function copy(destinationID, force) {
-
+ let destGroupID;
let sourceID = this.id;
- // allow force to be a string
- if (typeof force === "string") {
- force = (force.toLowerCase() === "true");
- } else {
- force = !!force;
- }
-
// Kick everyone from this pad.
// This was commented due to https://github.com/ether/etherpad-lite/issues/3183.
// Do we really need to kick everyone out?
@@ -375,31 +368,15 @@ Pad.prototype.copy = async function copy(destinationID, force) {
// flush the source pad:
this.saveToDatabase();
- // if it's a group pad, let's make sure the group exists.
- let destGroupID;
- if (destinationID.indexOf("$") >= 0) {
- destGroupID = destinationID.split("$")[0]
- let groupExists = await groupManager.doesGroupExist(destGroupID);
+ try {
+ // if it's a group pad, let's make sure the group exists.
+ destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID);
- // group does not exist
- if (!groupExists) {
- throw new customError("groupID does not exist for destinationID", "apierror");
- }
- }
-
- // if the pad exists, we should abort, unless forced.
- let exists = await padManager.doesPadExist(destinationID);
-
- if (exists) {
- if (!force) {
- console.error("erroring out without force");
- throw new customError("destinationID already exists", "apierror");
- }
-
- // exists and forcing
- let pad = await padManager.getPad(destinationID);
- await pad.remove();
+ // if force is true and already exists a Pad with the same id, remove that Pad
+ await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force);
+ } catch(err) {
+ throw err;
}
// copy the 'pad' entry
@@ -427,10 +404,7 @@ Pad.prototype.copy = async function copy(destinationID, force) {
promises.push(p);
}
- // add the new pad to all authors who contributed to the old one
- this.getAllAuthors().forEach(authorID => {
- authorManager.addPad(authorID, destinationID);
- });
+ this.copyAuthorInfoToDestinationPad(destinationID);
// wait for the above to complete
await Promise.all(promises);
@@ -452,6 +426,110 @@ Pad.prototype.copy = async function copy(destinationID, force) {
return { padID: destinationID };
}
+Pad.prototype.checkIfGroupExistAndReturnIt = async function checkIfGroupExistAndReturnIt(destinationID) {
+ let destGroupID = false;
+
+ if (destinationID.indexOf("$") >= 0) {
+
+ destGroupID = destinationID.split("$")[0]
+ let groupExists = await groupManager.doesGroupExist(destGroupID);
+
+ // group does not exist
+ if (!groupExists) {
+ throw new customError("groupID does not exist for destinationID", "apierror");
+ }
+ }
+ return destGroupID;
+}
+
+Pad.prototype.removePadIfForceIsTrueAndAlreadyExist = async function removePadIfForceIsTrueAndAlreadyExist(destinationID, force) {
+ // if the pad exists, we should abort, unless forced.
+ let exists = await padManager.doesPadExist(destinationID);
+
+ // allow force to be a string
+ if (typeof force === "string") {
+ force = (force.toLowerCase() === "true");
+ } else {
+ force = !!force;
+ }
+
+ if (exists) {
+ if (!force) {
+ console.error("erroring out without force");
+ throw new customError("destinationID already exists", "apierror");
+ }
+
+ // exists and forcing
+ let pad = await padManager.getPad(destinationID);
+ await pad.remove();
+ }
+}
+
+Pad.prototype.copyAuthorInfoToDestinationPad = function copyAuthorInfoToDestinationPad(destinationID) {
+ // add the new sourcePad to all authors who contributed to the old one
+ this.getAllAuthors().forEach(authorID => {
+ authorManager.addPad(authorID, destinationID);
+ });
+}
+
+Pad.prototype.copyPadWithoutHistory = async function copyPadWithoutHistory(destinationID, force) {
+ let destGroupID;
+ let sourceID = this.id;
+
+ // flush the source pad
+ this.saveToDatabase();
+
+ try {
+ // if it's a group pad, let's make sure the group exists.
+ destGroupID = await this.checkIfGroupExistAndReturnIt(destinationID);
+
+ // if force is true and already exists a Pad with the same id, remove that Pad
+ await this.removePadIfForceIsTrueAndAlreadyExist(destinationID, force);
+ } catch(err) {
+ throw err;
+ }
+
+ let sourcePad = await padManager.getPad(sourceID);
+
+ // add the new sourcePad to all authors who contributed to the old one
+ this.copyAuthorInfoToDestinationPad(destinationID);
+
+ // Group pad? Add it to the group's list
+ if (destGroupID) {
+ await db.setSub("group:" + destGroupID, ["pads", destinationID], 1);
+ }
+
+ // initialize the pad with a new line to avoid getting the defaultText
+ let newPad = await padManager.getPad(destinationID, '\n');
+
+ let oldAText = this.atext;
+ let newPool = newPad.pool;
+ newPool.fromJsonable(sourcePad.pool.toJsonable()); // copy that sourceId pool to the new pad
+
+ // based on Changeset.makeSplice
+ let assem = Changeset.smartOpAssembler();
+ assem.appendOpWithText('=', '');
+ Changeset.appendATextToAssembler(oldAText, assem);
+ assem.endDocument();
+
+ // although we have instantiated the newPad with '\n', an additional '\n' is
+ // added internally, so the pad text on the revision 0 is "\n\n"
+ let oldLength = 2;
+
+ let newLength = assem.getLengthChange();
+ let newText = oldAText.text;
+
+ // create a changeset that removes the previous text and add the newText with
+ // all atributes present on the source pad
+ let changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
+ newPad.appendRevision(changeset);
+
+ hooks.callAll('padCopy', { 'originalPad': this, 'destinationID': destinationID });
+
+ return { padID: destinationID };
+}
+
+
Pad.prototype.remove = async function remove() {
var padID = this.id;
diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js
index cac8c15c..cb714460 100644
--- a/src/node/handler/APIHandler.js
+++ b/src/node/handler/APIHandler.js
@@ -142,8 +142,13 @@ version["1.2.14"] = Object.assign({}, version["1.2.13"],
}
);
+version["1.2.15"] = Object.assign({}, version["1.2.14"],
+ { "copyPadWithoutHistory" : ["sourceID", "destinationID", "force"]
+ }
+);
+
// set the latest available API version here
-exports.latestApiVersion = '1.2.14';
+exports.latestApiVersion = '1.2.15';
// exports the versions so it can be used by the new Swagger endpoint
exports.version = version;
diff --git a/tests/backend/specs/api/pad.js b/tests/backend/specs/api/pad.js
index 95138c0b..6de05c8b 100644
--- a/tests/backend/specs/api/pad.js
+++ b/tests/backend/specs/api/pad.js
@@ -427,6 +427,7 @@ describe('deletePad', function(){
var originalPadId = testPadId;
var newPadId = makeid();
+var copiedPadId = makeid();
describe('createPad', function(){
it('creates a new Pad with text', function(done) {
@@ -681,12 +682,126 @@ describe('createPad', function(){
});
})
+describe('copyPad', function(){
+ it('copies the content of a existent pad', function(done) {
+ api.get(endPoint('copyPad')+"&sourceID="+testPadId+"&destinationID="+copiedPadId+"&force=true")
+ .expect(function(res){
+ if(res.body.code !== 0) throw new Error("Copy Pad Failed")
+ })
+ .expect('Content-Type', /json/)
+ .expect(200, done)
+ });
+})
+
+describe('copyPadWithoutHistory', function(){
+ var sourcePadId = makeid();
+ var newPad;
+
+ before(function(done) {
+ createNewPadWithHtml(sourcePadId, ulHtml, done);
+ });
+
+ beforeEach(function() {
+ newPad = makeid();
+ })
+
+ it('returns a successful response', function(done) {
+ api.get(endPoint('copyPadWithoutHistory')+"&sourceID="+sourcePadId+"&destinationID="+newPad+"&force=false")
+ .expect(function(res){
+ if(res.body.code !== 0) throw new Error("Copy Pad Without History Failed")
+ })
+ .expect('Content-Type', /json/)
+ .expect(200, done)
+ });
+
+ // this test validates if the source pad's text and attributes are kept
+ it('creates a new pad with the same content as the source pad', function(done) {
+ api.get(endPoint('copyPadWithoutHistory')+"&sourceID="+sourcePadId+"&destinationID="+newPad+"&force=false")
+ .expect(function(res){
+ if(res.body.code !== 0) throw new Error("Copy Pad Without History Failed")
+ })
+ .end(function() {
+ api.get(endPoint('getHTML')+"&padID="+newPad)
+ .expect(function(res){
+ var receivedHtml = res.body.data.html.replace("