pad.libre-service.eu-etherpad/src/node/db/Pad.js

751 lines
19 KiB
JavaScript
Raw Normal View History

2011-05-30 16:53:11 +02:00
/**
* The pad object, defined with joose
*/
2012-02-28 21:19:10 +01:00
var ERR = require("async-stacktrace");
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
2011-07-27 19:52:23 +02:00
var db = require("./DB").db;
var async = require("async");
2011-07-27 19:52:23 +02:00
var settings = require('../utils/Settings');
var authorManager = require("./AuthorManager");
2011-08-16 21:02:30 +02:00
var padManager = require("./PadManager");
var padMessageHandler = require("../handler/PadMessageHandler");
2013-11-17 17:46:43 +01:00
var groupManager = require("./GroupManager");
var customError = require("../utils/customError");
2011-08-16 21:02:30 +02:00
var readOnlyManager = require("./ReadOnlyManager");
var crypto = require("crypto");
2012-02-29 20:40:14 +01:00
var randomString = require("../utils/randomstring");
var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
//serialization/deserialization attributes
var attributeBlackList = ["id"];
var jsonableList = ["pool"];
/**
2011-05-30 16:53:11 +02:00
* Copied from the Etherpad source code. It converts Windows line breaks to Unix line breaks and convert Tabs to spaces
* @param txt
*/
exports.cleanText = function (txt) {
return txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\t/g, ' ').replace(/\xa0/g, ' ');
2012-01-30 15:59:13 +01:00
};
2012-01-30 15:59:13 +01:00
var Pad = function Pad(id) {
this.atext = Changeset.makeAText("\n");
this.pool = new AttributePool();
2012-01-30 15:59:13 +01:00
this.head = -1;
this.chatHead = -1;
this.publicStatus = false;
this.passwordHash = null;
this.id = id;
2012-02-29 20:40:14 +01:00
this.savedRevisions = [];
2012-01-30 15:59:13 +01:00
};
exports.Pad = Pad;
Pad.prototype.apool = function apool() {
return this.pool;
};
Pad.prototype.getHeadRevisionNumber = function getHeadRevisionNumber() {
return this.head;
};
Pad.prototype.getSavedRevisionsNumber = function getSavedRevisionsNumber() {
return this.savedRevisions.length;
};
Pad.prototype.getSavedRevisionsList = function getSavedRevisionsList() {
var savedRev = new Array();
for(var rev in this.savedRevisions){
savedRev.push(this.savedRevisions[rev].revNum);
}
savedRev.sort(function(a, b) {
return a - b;
});
return savedRev;
};
2012-01-30 15:59:13 +01:00
Pad.prototype.getPublicStatus = function getPublicStatus() {
return this.publicStatus;
};
Pad.prototype.appendRevision = function appendRevision(aChangeset, author) {
if(!author)
author = '';
var newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
Changeset.copyAText(newAText, this.atext);
var newRev = ++this.head;
var newRevData = {};
newRevData.changeset = aChangeset;
newRevData.meta = {};
newRevData.meta.author = author;
newRevData.meta.timestamp = new Date().getTime();
//ex. getNumForAuthor
if(author != '')
this.pool.putAttrib(['author', author || '']);
if(newRev % 100 == 0)
{
newRevData.meta.atext = this.atext;
}
db.set("pad:"+this.id+":revs:"+newRev, newRevData);
this.saveToDatabase();
// set the author to pad
if(author)
authorManager.addPad(author, this.id);
if (this.head == 0) {
hooks.callAll("padCreate", {'pad':this, 'author': author});
} else {
hooks.callAll("padUpdate", {'pad':this, 'author': author});
}
2012-01-30 15:59:13 +01:00
};
//save all attributes to the database
Pad.prototype.saveToDatabase = function saveToDatabase(){
var dbObject = {};
for(var attr in this){
if(typeof this[attr] === "function") continue;
if(attributeBlackList.indexOf(attr) !== -1) continue;
dbObject[attr] = this[attr];
if(jsonableList.indexOf(attr) !== -1){
dbObject[attr] = dbObject[attr].toJsonable();
}
}
db.set("pad:"+this.id, dbObject);
}
// get time of last edit (changeset application)
Pad.prototype.getLastEdit = function getLastEdit(callback){
var revNum = this.getHeadRevisionNumber();
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback);
}
2012-01-30 15:59:13 +01:00
Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum, callback) {
db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback);
};
Pad.prototype.getRevisionAuthor = function getRevisionAuthor(revNum, callback) {
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "author"], callback);
};
Pad.prototype.getRevisionDate = function getRevisionDate(revNum, callback) {
db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback);
};
Pad.prototype.getAllAuthors = function getAllAuthors() {
var authors = [];
2014-12-14 22:01:28 +01:00
for(var key in this.pool.numToAttrib)
2012-01-30 15:59:13 +01:00
{
if(this.pool.numToAttrib[key][0] == "author" && this.pool.numToAttrib[key][1] != "")
2011-08-01 19:45:28 +02:00
{
2012-01-30 15:59:13 +01:00
authors.push(this.pool.numToAttrib[key][1]);
}
}
return authors;
};
Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targetRev, callback) {
var _this = this;
var keyRev = this.getKeyRevisionNumber(targetRev);
var atext;
var changesets = [];
//find out which changesets are needed
var neededChangesets = [];
var curRev = keyRev;
while (curRev < targetRev)
{
curRev++;
neededChangesets.push(curRev);
}
async.series([
//get all needed data out of the database
function(callback)
2011-08-01 19:45:28 +02:00
{
2012-01-30 15:59:13 +01:00
async.parallel([
//get the atext of the key revision
function (callback)
2011-08-01 19:45:28 +02:00
{
2012-01-30 15:59:13 +01:00
db.getSub("pad:"+_this.id+":revs:"+keyRev, ["meta", "atext"], function(err, _atext)
{
if(ERR(err, callback)) return;
atext = Changeset.cloneAText(_atext);
callback();
});
},
//get all needed changesets
function (callback)
{
async.forEach(neededChangesets, function(item, callback)
{
_this.getRevisionChangeset(item, function(err, changeset)
{
if(ERR(err, callback)) return;
changesets[item] = changeset;
callback();
});
}, callback);
2011-08-01 19:45:28 +02:00
}
2012-01-30 15:59:13 +01:00
], callback);
2011-08-01 19:45:28 +02:00
},
2012-01-30 15:59:13 +01:00
//apply all changesets to the key changeset
function(callback)
2011-08-01 19:45:28 +02:00
{
2012-01-30 15:59:13 +01:00
var apool = _this.apool();
2011-08-01 19:45:28 +02:00
var curRev = keyRev;
2012-01-30 15:59:13 +01:00
while (curRev < targetRev)
2011-08-01 19:45:28 +02:00
{
curRev++;
2012-01-30 15:59:13 +01:00
var cs = changesets[curRev];
2013-12-17 16:20:57 +01:00
try{
atext = Changeset.applyToAText(cs, atext, apool);
}catch(e) {
return callback(e)
}
}
2012-01-30 15:59:13 +01:00
callback(null);
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, atext);
});
};
2013-01-23 00:37:53 +01:00
Pad.prototype.getRevision = function getRevisionChangeset(revNum, callback) {
db.get("pad:"+this.id+":revs:"+revNum, callback);
};
Pad.prototype.getAllAuthorColors = function getAllAuthorColors(callback){
var authors = this.getAllAuthors();
var returnTable = {};
var colorPalette = authorManager.getColorPalette();
async.forEach(authors, function(author, callback){
authorManager.getAuthorColorId(author, function(err, colorId){
if(err){
return callback(err);
}
2013-01-27 17:45:09 +01:00
//colorId might be a hex color or an number out of the palette
returnTable[author]=colorPalette[colorId] || colorId;
2013-01-23 00:37:53 +01:00
callback();
});
}, function(err){
callback(err, returnTable);
});
};
Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, endRev) {
startRev = parseInt(startRev, 10);
var head = this.getHeadRevisionNumber();
endRev = endRev ? parseInt(endRev, 10) : head;
if(isNaN(startRev) || startRev < 0 || startRev > head) {
startRev = null;
}
if(isNaN(endRev) || endRev < startRev) {
endRev = null;
} else if(endRev > head) {
endRev = head;
}
if(startRev !== null && endRev !== null) {
return { startRev: startRev , endRev: endRev }
}
return null;
};
2012-01-30 15:59:13 +01:00
Pad.prototype.getKeyRevisionNumber = function getKeyRevisionNumber(revNum) {
return Math.floor(revNum / 100) * 100;
};
Pad.prototype.text = function text() {
return this.atext.text;
};
Pad.prototype.setText = function setText(newText) {
//clean the new text
newText = exports.cleanText(newText);
var oldText = this.text();
//create the changeset
// We want to ensure the pad still ends with a \n, but otherwise keep
// getText() and setText() consistent.
var changeset;
if (newText[newText.length - 1] == '\n') {
changeset = Changeset.makeSplice(oldText, 0, oldText.length, newText);
} else {
changeset = Changeset.makeSplice(oldText, 0, oldText.length-1, newText);
}
2012-01-30 15:59:13 +01:00
//append the changeset
this.appendRevision(changeset);
};
Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) {
this.chatHead++;
//save the chat entry in the database
db.set("pad:"+this.id+":chat:"+this.chatHead, {"text": text, "userId": userId, "time": time});
this.saveToDatabase();
2012-01-30 15:59:13 +01:00
};
Pad.prototype.getChatMessage = function getChatMessage(entryNum, callback) {
var _this = this;
var entry;
async.series([
//get the chat entry
function(callback)
{
db.get("pad:"+_this.id+":chat:"+entryNum, function(err, _entry)
2011-08-01 19:45:28 +02:00
{
if(ERR(err, callback)) return;
2012-01-30 15:59:13 +01:00
entry = _entry;
callback();
2011-08-01 19:45:28 +02:00
});
},
2012-01-30 15:59:13 +01:00
//add the authorName
function(callback)
2011-08-01 19:45:28 +02:00
{
2012-01-30 15:59:13 +01:00
//this chat message doesn't exist, return null
if(entry == null)
2011-08-01 19:45:28 +02:00
{
2012-01-30 15:59:13 +01:00
callback();
2011-08-01 19:45:28 +02:00
return;
}
2012-01-30 15:59:13 +01:00
//get the authorName
authorManager.getAuthorName(entry.userId, function(err, authorName)
2011-08-01 19:45:28 +02:00
{
if(ERR(err, callback)) return;
2012-01-30 15:59:13 +01:00
entry.userName = authorName;
callback();
2011-08-01 19:45:28 +02:00
});
2012-01-30 15:59:13 +01:00
}
], function(err)
{
if(ERR(err, callback)) return;
callback(null, entry);
});
};
Pad.prototype.getChatMessages = function getChatMessages(start, end, callback) {
2012-01-30 15:59:13 +01:00
//collect the numbers of chat entries and in which order we need them
var neededEntries = [];
var order = 0;
for(var i=start;i<=end; i++)
{
neededEntries.push({entryNum:i, order: order});
order++;
}
var _this = this;
2012-01-30 15:59:13 +01:00
//get all entries out of the database
var entries = [];
async.forEach(neededEntries, function(entryObject, callback)
{
_this.getChatMessage(entryObject.entryNum, function(err, entry)
{
if(ERR(err, callback)) return;
entries[entryObject.order] = entry;
callback();
});
}, function(err)
{
if(ERR(err, callback)) return;
//sort out broken chat entries
//it looks like in happend in the past that the chat head was
//incremented, but the chat message wasn't added
var cleanedEntries = [];
for(var i=0;i<entries.length;i++)
{
if(entries[i]!=null)
cleanedEntries.push(entries[i]);
else
console.warn("WARNING: Found broken chat entry in pad " + _this.id);
}
callback(null, cleanedEntries);
});
};
Pad.prototype.init = function init(text, callback) {
var _this = this;
//replace text with default text if text isn't set
if(text == null)
{
text = settings.defaultPadText;
}
//try to load the pad
db.get("pad:"+this.id, function(err, value)
{
if(ERR(err, callback)) return;
//if this pad exists, load it
if(value != null)
{
//copy all attr. To a transfrom via fromJsonable if necassary
for(var attr in value){
if(jsonableList.indexOf(attr) !== -1){
_this[attr] = _this[attr].fromJsonable(value[attr]);
} else {
_this[attr] = value[attr];
}
}
2012-01-30 15:59:13 +01:00
}
//this pad doesn't exist, so create it
else
{
var firstChangeset = Changeset.makeSplice("\n", 0, 0, exports.cleanText(text));
_this.appendRevision(firstChangeset, '');
}
hooks.callAll("padLoad", {'pad':_this});
2012-01-30 15:59:13 +01:00
callback(null);
});
};
Pad.prototype.copy = function copy(destinationID, force, callback) {
2013-11-17 20:01:02 +01:00
var sourceID = this.id;
2013-11-17 17:46:43 +01:00
var _this = this;
var destGroupID;
2013-11-17 17:46:43 +01:00
// make force optional
if (typeof force == "function") {
callback = force;
force = false;
}
else if (force == undefined || force.toLowerCase() != "true") {
force = false;
}
else force = true;
2013-11-17 17:46:43 +01:00
//kick everyone from this pad
// TODO: this presents a message on the client saying that the pad was 'deleted'. Fix this?
2013-11-17 20:01:02 +01:00
padMessageHandler.kickSessionsFromPad(sourceID);
2013-11-17 17:46:43 +01:00
// flush the source pad:
_this.saveToDatabase();
async.series([
// if it's a group pad, let's make sure the group exists.
function(callback)
{
if (destinationID.indexOf("$") != -1)
{
destGroupID = destinationID.split("$")[0]
groupManager.doesGroupExist(destGroupID, function (err, exists)
2013-11-17 17:46:43 +01:00
{
if(ERR(err, callback)) return;
2013-11-17 17:46:43 +01:00
//group does not exist
if(exists == false)
{
callback(new customError("groupID does not exist for destinationID","apierror"));
return;
2013-11-17 17:46:43 +01:00
}
//everything is fine, continue
else
{
callback();
}
});
}
else
callback();
2013-11-17 17:46:43 +01:00
},
// if the pad exists, we should abort, unless forced.
2013-11-17 17:46:43 +01:00
function(callback)
{
padManager.doesPadExists(destinationID, function (err, exists)
2013-11-17 17:46:43 +01:00
{
if(ERR(err, callback)) return;
2013-11-17 17:46:43 +01:00
if(exists == true)
{
if (!force)
{
console.error("erroring out without force");
callback(new customError("destinationID already exists","apierror"));
console.error("erroring out without force - after");
return;
}
else // exists and forcing
{
padManager.getPad(destinationID, function(err, pad) {
if (ERR(err, callback)) return;
pad.remove(callback);
});
}
2013-11-17 17:46:43 +01:00
}
else
2013-11-17 17:46:43 +01:00
{
callback();
2013-11-17 17:46:43 +01:00
}
});
},
// copy the 'pad' entry
function(callback)
{
db.get("pad:"+sourceID, function(err, pad) {
db.set("pad:"+destinationID, pad);
});
callback();
},
//copy all relations
2013-11-17 17:46:43 +01:00
function(callback)
{
async.parallel([
//copy all chat messages
function(callback)
{
var chatHead = _this.chatHead;
for(var i=0;i<=chatHead;i++)
{
2013-11-17 20:01:02 +01:00
db.get("pad:"+sourceID+":chat:"+i, function (err, chat) {
if (ERR(err, callback)) return;
2013-11-17 20:01:02 +01:00
db.set("pad:"+destinationID+":chat:"+i, chat);
2013-11-17 17:46:43 +01:00
});
}
callback();
},
//copy all revisions
function(callback)
{
var revHead = _this.head;
for(var i=0;i<=revHead;i++)
{
2013-11-17 20:01:02 +01:00
db.get("pad:"+sourceID+":revs:"+i, function (err, rev) {
2013-11-17 17:46:43 +01:00
if (ERR(err, callback)) return;
2013-11-17 20:01:02 +01:00
db.set("pad:"+destinationID+":revs:"+i, rev);
2013-11-17 17:46:43 +01:00
});
}
callback();
},
//add the new pad to all authors who contributed to the old one
function(callback)
{
var authorIDs = _this.getAllAuthors();
authorIDs.forEach(function (authorID)
{
2013-11-17 20:01:02 +01:00
authorManager.addPad(authorID, destinationID);
2013-11-17 17:46:43 +01:00
});
callback();
},
// parallel
], callback);
},
function(callback) {
// Group pad? Add it to the group's list
if(destGroupID) db.setSub("group:" + destGroupID, ["pads", destinationID], 1);
// Initialize the new pad (will update the listAllPads cache)
setTimeout(function(){
padManager.getPad(destinationID, null, callback) // this runs too early.
},10);
}
2013-11-17 17:46:43 +01:00
// series
], function(err)
{
if(ERR(err, callback)) return;
2013-11-17 20:01:02 +01:00
callback(null, {padID: destinationID});
2013-11-17 17:46:43 +01:00
});
};
2012-01-30 15:59:13 +01:00
Pad.prototype.remove = function remove(callback) {
var padID = this.id;
var _this = this;
//kick everyone from this pad
padMessageHandler.kickSessionsFromPad(padID);
async.series([
//delete all relations
function(callback)
2011-08-16 21:02:30 +02:00
{
2012-01-30 15:59:13 +01:00
async.parallel([
//is it a group pad? -> delete the entry of this pad in the group
2011-08-16 21:02:30 +02:00
function(callback)
{
2012-01-30 15:59:13 +01:00
//is it a group pad?
if(padID.indexOf("$")!=-1)
{
var groupID = padID.substring(0,padID.indexOf("$"));
db.get("group:" + groupID, function (err, group)
2011-08-16 21:02:30 +02:00
{
2012-01-30 15:59:13 +01:00
if(ERR(err, callback)) return;
//remove the pad entry
delete group.pads[padID];
//set the new value
db.set("group:" + groupID, group);
2011-08-16 21:02:30 +02:00
callback();
2012-01-30 15:59:13 +01:00
});
}
//its no group pad, nothing to do here
else
{
callback();
}
2011-08-16 21:02:30 +02:00
},
2012-01-30 15:59:13 +01:00
//remove the readonly entries
2011-08-16 21:02:30 +02:00
function(callback)
{
2012-01-30 15:59:13 +01:00
readOnlyManager.getReadOnlyId(padID, function(err, readonlyID)
{
if(ERR(err, callback)) return;
db.remove("pad2readonly:" + padID);
db.remove("readonly2pad:" + readonlyID);
callback();
});
},
//delete all chat messages
function(callback)
{
var chatHead = _this.chatHead;
for(var i=0;i<=chatHead;i++)
{
db.remove("pad:"+padID+":chat:"+i);
}
callback();
},
//delete all revisions
function(callback)
{
var revHead = _this.head;
for(var i=0;i<=revHead;i++)
{
db.remove("pad:"+padID+":revs:"+i);
}
callback();
},
//remove pad from all authors who contributed
function(callback)
{
var authorIDs = _this.getAllAuthors();
authorIDs.forEach(function (authorID)
{
authorManager.removePad(authorID, padID);
});
2011-08-16 21:02:30 +02:00
callback();
}
2012-01-30 15:59:13 +01:00
], callback);
2011-08-16 21:02:30 +02:00
},
2012-01-30 15:59:13 +01:00
//delete the pad entry and delete pad from padManager
function(callback)
{
padManager.removePad(padID);
hooks.callAll("padRemove", {'padID':padID});
2012-01-30 15:59:13 +01:00
callback();
}
2012-01-30 15:59:13 +01:00
], function(err)
{
if(ERR(err, callback)) return;
callback();
});
};
//set in db
Pad.prototype.setPublicStatus = function setPublicStatus(publicStatus) {
this.publicStatus = publicStatus;
this.saveToDatabase();
2012-01-30 15:59:13 +01:00
};
Pad.prototype.setPassword = function setPassword(password) {
this.passwordHash = password == null ? null : hash(password, generateSalt());
this.saveToDatabase();
2012-01-30 15:59:13 +01:00
};
Pad.prototype.isCorrectPassword = function isCorrectPassword(password) {
return compare(this.passwordHash, password);
};
Pad.prototype.isPasswordProtected = function isPasswordProtected() {
return this.passwordHash != null;
};
2012-02-29 20:40:14 +01:00
Pad.prototype.addSavedRevision = function addSavedRevision(revNum, savedById, label) {
//if this revision is already saved, return silently
for(var i in this.savedRevisions){
2014-12-17 01:10:20 +01:00
if(this.savedRevisions[i] && this.savedRevisions[i].revNum === revNum){
2012-02-29 20:40:14 +01:00
return;
}
}
2012-02-29 20:40:14 +01:00
//build the saved revision object
var savedRevision = {};
savedRevision.revNum = revNum;
savedRevision.savedById = savedById;
savedRevision.label = label || "Revision " + revNum;
savedRevision.timestamp = new Date().getTime();
savedRevision.id = randomString(10);
2012-02-29 20:40:14 +01:00
//save this new saved revision
this.savedRevisions.push(savedRevision);
this.saveToDatabase();
};
Pad.prototype.getSavedRevisions = function getSavedRevisions() {
return this.savedRevisions;
};
/* Crypto helper methods */
function hash(password, salt)
{
var shasum = crypto.createHash('sha512');
shasum.update(password + salt);
return shasum.digest("hex") + "$" + salt;
}
function generateSalt()
{
return randomString(86);
}
function compare(hashStr, password)
{
2012-01-30 15:59:13 +01:00
return hash(password, hashStr.split("$")[1]) === hashStr;
}