summaryrefslogtreecommitdiff
path: root/src/node
diff options
context:
space:
mode:
Diffstat (limited to 'src/node')
-rw-r--r--src/node/db/API.js216
-rw-r--r--src/node/db/AuthorManager.js3
-rw-r--r--src/node/db/GroupManager.js27
-rw-r--r--src/node/db/Pad.js157
-rw-r--r--src/node/db/PadManager.js60
-rw-r--r--src/node/db/ReadOnlyManager.js34
-rw-r--r--src/node/db/SecurityManager.js48
-rw-r--r--src/node/db/SessionManager.js16
-rw-r--r--src/node/db/SessionStore.js2
-rw-r--r--src/node/handler/APIHandler.js167
-rw-r--r--src/node/handler/ExportHandler.js300
-rw-r--r--src/node/handler/ImportHandler.js95
-rw-r--r--src/node/handler/PadMessageHandler.js471
-rw-r--r--src/node/handler/SocketIORouter.js101
-rw-r--r--src/node/hooks/express.js8
-rw-r--r--src/node/hooks/express/adminplugins.js77
-rw-r--r--src/node/hooks/express/apicalls.js12
-rw-r--r--src/node/hooks/express/errorhandling.js7
-rw-r--r--src/node/hooks/express/importexport.js2
-rw-r--r--src/node/hooks/express/padreadonly.js56
-rw-r--r--src/node/hooks/express/padurlsanitize.js22
-rw-r--r--src/node/hooks/express/specialpages.js32
-rw-r--r--src/node/hooks/express/swagger.js5
-rw-r--r--src/node/hooks/express/webaccess.js20
-rwxr-xr-xsrc/node/server.js6
-rw-r--r--src/node/stats.js3
-rw-r--r--src/node/utils/Abiword.js18
-rw-r--r--src/node/utils/ExportDokuWiki.js2
-rw-r--r--src/node/utils/ExportHelper.js8
-rw-r--r--src/node/utils/ExportHtml.js199
-rw-r--r--src/node/utils/ExportTxt.js3
-rw-r--r--src/node/utils/ImportHtml.js23
-rw-r--r--src/node/utils/Minify.js10
-rw-r--r--src/node/utils/Settings.js56
-rw-r--r--src/node/utils/caching_middleware.js6
-rw-r--r--src/node/utils/padDiff.js38
-rw-r--r--src/node/utils/randomstring.js13
-rw-r--r--src/node/utils/tar.json1
-rw-r--r--src/node/utils/toolbar.js239
39 files changed, 1779 insertions, 784 deletions
diff --git a/src/node/db/API.js b/src/node/db/API.js
index 3955d495..4a912368 100644
--- a/src/node/db/API.js
+++ b/src/node/db/API.js
@@ -75,6 +75,129 @@ exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor;
/************************/
/**
+getAttributePool(padID) returns the attribute pool of a pad
+
+Example returns:
+{
+ "code":0,
+ "message":"ok",
+ "data": {
+ "pool":{
+ "numToAttrib":{
+ "0":["author","a.X4m8bBWJBZJnWGSh"],
+ "1":["author","a.TotfBPzov54ihMdH"],
+ "2":["author","a.StiblqrzgeNTbK05"],
+ "3":["bold","true"]
+ },
+ "attribToNum":{
+ "author,a.X4m8bBWJBZJnWGSh":0,
+ "author,a.TotfBPzov54ihMdH":1,
+ "author,a.StiblqrzgeNTbK05":2,
+ "bold,true":3
+ },
+ "nextNum":4
+ }
+ }
+}
+
+*/
+exports.getAttributePool = function (padID, callback)
+{
+ getPadSafe(padID, true, function(err, pad)
+ {
+ if (ERR(err, callback)) return;
+ callback(null, {pool: pad.pool});
+ });
+}
+
+/**
+getRevisionChangeset (padID, [rev])
+
+get the changeset at a given revision, or last revision if 'rev' is not defined.
+
+Example returns:
+{
+ "code" : 0,
+ "message" : "ok",
+ "data" : "Z:1>6b|5+6b$Welcome to Etherpad!\n\nThis 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!\n\nGet involved with Etherpad at http://etherpad.org\n"
+}
+
+*/
+exports.getRevisionChangeset = function(padID, rev, callback)
+{
+ // check if rev is set
+ if (typeof rev === "function")
+ {
+ callback = rev;
+ rev = undefined;
+ }
+
+ // check if rev is a number
+ if (rev !== undefined && typeof rev !== "number")
+ {
+ // try to parse the number
+ if (!isNaN(parseInt(rev)))
+ {
+ rev = parseInt(rev);
+ }
+ else
+ {
+ callback(new customError("rev is not a number", "apierror"));
+ return;
+ }
+ }
+
+ // ensure this is not a negative number
+ if (rev !== undefined && rev < 0)
+ {
+ callback(new customError("rev is not a negative number", "apierror"));
+ return;
+ }
+
+ // ensure this is not a float value
+ if (rev !== undefined && !is_int(rev))
+ {
+ callback(new customError("rev is a float value", "apierror"));
+ return;
+ }
+
+ // get the pad
+ getPadSafe(padID, true, function(err, pad)
+ {
+ if(ERR(err, callback)) return;
+
+ //the client asked for a special revision
+ if(rev !== undefined)
+ {
+ //check if this is a valid revision
+ if(rev > pad.getHeadRevisionNumber())
+ {
+ callback(new customError("rev is higher than the head revision of the pad","apierror"));
+ return;
+ }
+
+ //get the changeset for this revision
+ pad.getRevisionChangeset(rev, function(err, changeset)
+ {
+ if(ERR(err, callback)) return;
+
+ callback(null, changeset);
+ })
+ }
+ //the client wants the latest changeset, lets return it to him
+ else
+ {
+ pad.getRevisionChangeset(pad.getHeadRevisionNumber(), function(err, changeset)
+ {
+ if(ERR(err, callback)) return;
+
+ callback(null, changeset);
+ })
+ }
+ });
+}
+
+/**
getText(padID, [rev]) returns the text of a pad
Example returns:
@@ -243,6 +366,8 @@ exports.getHTML = function(padID, rev, callback)
exportHtml.getPadHTML(pad, rev, function(err, html)
{
if(ERR(err, callback)) return;
+ html = "<!DOCTYPE HTML><html><body>" +html; // adds HTML head
+ html += "</body></html>";
data = {html: html};
callback(null, data);
});
@@ -253,6 +378,8 @@ exports.getHTML = function(padID, rev, callback)
exportHtml.getPadHTML(pad, undefined, function (err, html)
{
if(ERR(err, callback)) return;
+ html = "<!DOCTYPE HTML><html><body>" +html; // adds HTML head
+ html += "</body></html>";
data = {html: html};
callback(null, data);
});
@@ -260,15 +387,30 @@ exports.getHTML = function(padID, rev, callback)
});
}
+/**
+setHTML(padID, html) 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 = function(padID, html, callback)
{
+ //html is required
+ if(typeof html != "string")
+ {
+ callback(new customError("html is no string","apierror"));
+ return;
+ }
+
//get the pad
getPadSafe(padID, true, function(err, pad)
{
if(ERR(err, callback)) return;
// add a new changeset with the new html to the pad
- importHtml.setPadHTML(pad, cleanText(html));
+ importHtml.setPadHTML(pad, cleanText(html), callback);
//update the clients on the pad
padMessageHandler.updatePadClients(pad, callback);
@@ -322,8 +464,8 @@ exports.getChatHistory = function(padID, start, end, callback)
// fall back to getting the whole chat-history if a parameter is missing
if(!start || !end)
{
- start = 0;
- end = pad.chatHead;
+ start = 0;
+ end = pad.chatHead;
}
if(start >= chatHead && chatHead > 0)
@@ -435,6 +577,46 @@ exports.deletePad = function(padID, callback)
}
/**
+copyPad(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.copyPad = function(sourceID, destinationID, force, callback)
+{
+ getPadSafe(sourceID, true, function(err, pad)
+ {
+ if(ERR(err, callback)) return;
+
+ pad.copy(destinationID, force, callback);
+ });
+}
+
+/**
+movePad(sourceID, destinationID[, force=false]) moves 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.movePad = function(sourceID, destinationID, force, callback)
+{
+ getPadSafe(sourceID, true, function(err, pad)
+ {
+ if(ERR(err, callback)) return;
+
+ pad.copy(destinationID, force, function(err) {
+ if(ERR(err, callback)) return;
+ pad.remove(callback);
+ });
+ });
+}
+/**
getReadOnlyLink(padID) returns the read only link of a pad
Example returns:
@@ -459,6 +641,32 @@ exports.getReadOnlyID = function(padID, callback)
}
/**
+getPadID(roID) returns the padID of a pad based on the readonlyID(roID)
+
+Example returns:
+
+{code: 0, message:"ok", data: {padID: padID}}
+{code: 1, message:"padID does not exist", data: null}
+*/
+exports.getPadID = function(roID, callback)
+{
+ //get the PadId
+ readOnlyManager.getPadId(roID, function(err, retrievedPadID)
+ {
+ if(ERR(err, callback)) return;
+
+ if(retrievedPadID == null)
+ {
+ callback(new customError("padID does not exist","apierror"));
+ }
+ else
+ {
+ callback(null, {padID: retrievedPadID});
+ }
+ });
+}
+
+/**
setPublicStatus(padID, publicStatus) sets a boolean for the public status of a pad
Example returns:
@@ -660,7 +868,7 @@ createDiffHTML(padID, startRev, endRev) returns an object of diffs from 2 points
Example returns:
-{"code":0,"message":"ok","data":{"html":"<style>\n.authora_HKIv23mEbachFYfH {background-color: #a979d9}\n.authora_n4gEeMLsv1GivNeh {background-color: #a9b5d9}\n.removed {text-decoration: line-through; -ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=80)'; filter: alpha(opacity=80); opacity: 0.8; }\n</style>Welcome to Etherpad Lite!<br><br>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!<br><br>Get involved with Etherpad at <a href=\"http&#x3a;&#x2F;&#x2F;etherpad&#x2e;org\">http:&#x2F;&#x2F;etherpad.org</a><br><span class=\"authora_HKIv23mEbachFYfH\">aw</span><br><br>","authors":["a.HKIv23mEbachFYfH",""]}}
+{"code":0,"message":"ok","data":{"html":"<style>\n.authora_HKIv23mEbachFYfH {background-color: #a979d9}\n.authora_n4gEeMLsv1GivNeh {background-color: #a9b5d9}\n.removed {text-decoration: line-through; -ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=80)'; filter: alpha(opacity=80); opacity: 0.8; }\n</style>Welcome to Etherpad!<br><br>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!<br><br>Get involved with Etherpad at <a href=\"http&#x3a;&#x2F;&#x2F;etherpad&#x2e;org\">http:&#x2F;&#x2F;etherpad.org</a><br><span class=\"authora_HKIv23mEbachFYfH\">aw</span><br><br>","authors":["a.HKIv23mEbachFYfH",""]}}
{"code":4,"message":"no or wrong API Key","data":null}
*/
exports.createDiffHTML = function(padID, startRev, endRev, callback){
diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js
index 667e0605..5ba608e9 100644
--- a/src/node/db/AuthorManager.js
+++ b/src/node/db/AuthorManager.js
@@ -22,6 +22,7 @@
var ERR = require("async-stacktrace");
var db = require("./DB").db;
var async = require("async");
+var customError = require("../utils/customError");
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
exports.getColorPalette = function(){
@@ -272,4 +273,4 @@ exports.removePad = function (authorID, padID)
db.set("globalAuthor:" + authorID, author);
}
});
-} \ No newline at end of file
+}
diff --git a/src/node/db/GroupManager.js b/src/node/db/GroupManager.js
index 81b0cb9e..82c14c39 100644
--- a/src/node/db/GroupManager.js
+++ b/src/node/db/GroupManager.js
@@ -215,25 +215,32 @@ exports.createGroupIfNotExistsFor = function(groupMapper, callback)
{
if(ERR(err, callback)) return;
+ // there is a group for this mapper
+ if(groupID) {
+ exports.doesGroupExist(groupID, function(err, exists) {
+ if(ERR(err, callback)) return;
+ if(exists) return callback(null, {groupID: groupID});
+
+ // hah, the returned group doesn't exist, let's create one
+ createGroupForMapper(callback)
+ })
+ }
//there is no group for this mapper, let's create a group
- if(groupID == null)
- {
+ else {
+ createGroupForMapper(callback)
+ }
+
+ function createGroupForMapper(cb) {
exports.createGroup(function(err, responseObj)
{
- if(ERR(err, callback)) return;
+ if(ERR(err, cb)) return;
//create the mapper entry for this group
db.set("mapper2group:"+groupMapper, responseObj.groupID);
- callback(null, responseObj);
+ cb(null, responseObj);
});
}
- //there is a group for this mapper, let's return it
- else
- {
- if(ERR(err, callback)) return;
- callback(null, {groupID: groupID});
- }
});
}
diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js
index 4701e82a..c7e0d50a 100644
--- a/src/node/db/Pad.js
+++ b/src/node/db/Pad.js
@@ -6,13 +6,14 @@
var ERR = require("async-stacktrace");
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var AttributePool = require("ep_etherpad-lite/static/js/AttributePool");
-var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
var db = require("./DB").db;
var async = require("async");
var settings = require('../utils/Settings');
var authorManager = require("./AuthorManager");
var padManager = require("./PadManager");
var padMessageHandler = require("../handler/PadMessageHandler");
+var groupManager = require("./GroupManager");
+var customError = require("../utils/customError");
var readOnlyManager = require("./ReadOnlyManager");
var crypto = require("crypto");
var randomString = require("../utils/randomstring");
@@ -201,7 +202,11 @@ Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targe
{
curRev++;
var cs = changesets[curRev];
- atext = Changeset.applyToAText(cs, atext, apool);
+ try{
+ atext = Changeset.applyToAText(cs, atext, apool);
+ }catch(e) {
+ return callback(e)
+ }
}
callback(null);
@@ -404,6 +409,152 @@ Pad.prototype.init = function init(text, callback) {
});
};
+Pad.prototype.copy = function copy(destinationID, force, callback) {
+ var sourceID = this.id;
+ var _this = this;
+
+ // make force optional
+ if (typeof force == "function") {
+ callback = force;
+ force = false;
+ }
+ else if (force == undefined || force.toLowerCase() != "true") {
+ force = false;
+ }
+ else force = true;
+
+ //kick everyone from this pad
+ // TODO: this presents a message on the client saying that the pad was 'deleted'. Fix this?
+ padMessageHandler.kickSessionsFromPad(sourceID);
+
+ // 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)
+ {
+ groupManager.doesGroupExist(destinationID.split("$")[0], function (err, exists)
+ {
+ if(ERR(err, callback)) return;
+
+ //group does not exist
+ if(exists == false)
+ {
+ callback(new customError("groupID does not exist for destinationID","apierror"));
+ return;
+ }
+ //everything is fine, continue
+ else
+ {
+ callback();
+ }
+ });
+ }
+ else
+ callback();
+ },
+ // if the pad exists, we should abort, unless forced.
+ function(callback)
+ {
+ console.log("destinationID", destinationID, force);
+ padManager.doesPadExists(destinationID, function (err, exists)
+ {
+ if(ERR(err, callback)) return;
+
+ if(exists == true)
+ {
+ if (!force)
+ {
+ console.log("erroring out without force");
+ callback(new customError("destinationID already exists","apierror"));
+ console.log("erroring out without force - after");
+ return;
+ }
+ else // exists and forcing
+ {
+ padManager.getPad(destinationID, function(err, pad) {
+ if (ERR(err, callback)) return;
+ pad.remove(callback);
+ });
+ }
+ }
+ else
+ {
+ callback();
+ }
+ });
+ },
+ // copy the 'pad' entry
+ function(callback)
+ {
+ db.get("pad:"+sourceID, function(err, pad) {
+ db.set("pad:"+destinationID, pad);
+ });
+ callback();
+ },
+ //copy all relations
+ function(callback)
+ {
+ async.parallel([
+ //copy all chat messages
+ function(callback)
+ {
+ var chatHead = _this.chatHead;
+
+ for(var i=0;i<=chatHead;i++)
+ {
+ db.get("pad:"+sourceID+":chat:"+i, function (err, chat) {
+ if (ERR(err, callback)) return;
+ db.set("pad:"+destinationID+":chat:"+i, chat);
+ });
+ }
+
+ callback();
+ },
+ //copy all revisions
+ function(callback)
+ {
+ var revHead = _this.head;
+ //console.log(revHead);
+ for(var i=0;i<=revHead;i++)
+ {
+ db.get("pad:"+sourceID+":revs:"+i, function (err, rev) {
+ //console.log("HERE");
+
+ if (ERR(err, callback)) return;
+ db.set("pad:"+destinationID+":revs:"+i, rev);
+ });
+ }
+
+ callback();
+ },
+ //add the new pad to all authors who contributed to the old one
+ function(callback)
+ {
+ var authorIDs = _this.getAllAuthors();
+
+ authorIDs.forEach(function (authorID)
+ {
+ console.log("authors");
+ authorManager.addPad(authorID, destinationID);
+ });
+
+ callback();
+ },
+ // parallel
+ ], callback);
+ },
+ // series
+ ], function(err)
+ {
+ if(ERR(err, callback)) return;
+ callback(null, {padID: destinationID});
+ });
+};
+
Pad.prototype.remove = function remove(callback) {
var padID = this.id;
var _this = this;
@@ -487,7 +638,7 @@ Pad.prototype.remove = function remove(callback) {
authorIDs.forEach(function (authorID)
{
- authorManager.removePad(authorID, padID);
+ authorManager.removePad(authorID, padID);
});
callback();
diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js
index 7d546fc7..2ecd6e27 100644
--- a/src/node/db/PadManager.js
+++ b/src/node/db/PadManager.js
@@ -24,7 +24,9 @@ var Pad = require("../db/Pad").Pad;
var db = require("./DB").db;
/**
- * An Object containing all known Pads. Provides "get" and "set" functions,
+ * A cache of all loaded Pads.
+ *
+ * Provides "get" and "set" functions,
* which should be used instead of indexing with brackets. These prepend a
* colon to the key, to avoid conflicting with built-in Object methods or with
* these functions themselves.
@@ -37,39 +39,55 @@ var globalPads = {
set: function (name, value)
{
this[':'+name] = value;
- padList.addPad(name);
},
- remove: function (name) { delete this[':'+name]; }
+ remove: function (name) {
+ delete this[':'+name];
+ }
};
+/**
+ * A cache of the list of all pads.
+ *
+ * Updated without db access as new pads are created/old ones removed.
+ */
var padList = {
list: [],
sorted : false,
- init: function()
+ initiated: false,
+ init: function(cb)
{
db.findKeys("pad:*", "*:*:*", function(err, dbData)
{
- if(ERR(err)) return;
+ if(ERR(err, cb)) return;
if(dbData != null){
+ padList.initiated = true
dbData.forEach(function(val){
padList.addPad(val.replace(/pad:/,""),false);
});
+ cb && cb()
}
});
return this;
},
+ load: function(cb) {
+ if(this.initiated) cb && cb()
+ else this.init(cb)
+ },
/**
* Returns all pads in alphabetical order as array.
*/
- getPads: function(){
- if(!this.sorted){
- this.list=this.list.sort();
- this.sorted=true;
- }
- return this.list;
+ getPads: function(cb){
+ this.load(function() {
+ if(!padList.sorted){
+ padList.list = padList.list.sort();
+ padList.sorted = true;
+ }
+ cb && cb(padList.list);
+ })
},
addPad: function(name)
{
+ if(!this.initiated) return;
if(this.list.indexOf(name) == -1){
this.list.push(name);
this.sorted=false;
@@ -77,7 +95,8 @@ var padList = {
},
removePad: function(name)
{
- var index=this.list.indexOf(name);
+ if(!this.initiated) return;
+ var index = this.list.indexOf(name);
if(index>-1){
this.list.splice(index,1);
this.sorted=false;
@@ -85,7 +104,6 @@ var padList = {
}
};
//initialises the allknowing data structure
-padList.init();
/**
* An array of padId transformations. These represent changes in pad name policy over
@@ -152,18 +170,17 @@ exports.getPad = function(id, text, callback)
{
if(ERR(err, callback)) return;
globalPads.set(id, pad);
+ padList.addPad(id);
callback(null, pad);
});
}
}
-exports.listAllPads = function(callback)
+exports.listAllPads = function(cb)
{
- if(callback != null){
- callback(null,{padIDs: padList.getPads()});
- }else{
- return {padIDs: padList.getPads()};
- }
+ padList.getPads(function(list) {
+ cb && cb(null, {padIDs: list});
+ });
}
//checks if a pad exists
@@ -229,9 +246,8 @@ exports.removePad = function(padId){
padList.removePad(padId);
}
-//removes a pad from the array
+//removes a pad from the cache
exports.unloadPad = function(padId)
{
- if(globalPads.get(padId))
- globalPads.remove(padId);
+ globalPads.remove(padId);
}
diff --git a/src/node/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js
index b135e613..f49f71e2 100644
--- a/src/node/db/ReadOnlyManager.js
+++ b/src/node/db/ReadOnlyManager.js
@@ -22,7 +22,7 @@
var ERR = require("async-stacktrace");
var db = require("./DB").db;
var async = require("async");
-var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
+var randomString = require("../utils/randomstring");
/**
* returns a read only id for a pad
@@ -77,28 +77,22 @@ exports.getPadId = function(readOnlyId, callback)
* returns a the padId and readonlyPadId in an object for any id
* @param {String} padIdOrReadonlyPadId read only id or real pad id
*/
-exports.getIds = function(padIdOrReadonlyPadId, callback) {
- var handleRealPadId = function () {
- exports.getReadOnlyId(padIdOrReadonlyPadId, function (err, value) {
+exports.getIds = function(id, callback) {
+ if (id.indexOf("r.") == 0)
+ exports.getPadId(id, function (err, value) {
+ if(ERR(err, callback)) return;
+ callback(null, {
+ readOnlyPadId: id,
+ padId: value, // Might be null, if this is an unknown read-only id
+ readonly: true
+ });
+ });
+ else
+ exports.getReadOnlyId(id, function (err, value) {
callback(null, {
readOnlyPadId: value,
- padId: padIdOrReadonlyPadId,
+ padId: id,
readonly: false
});
});
- }
-
- if (padIdOrReadonlyPadId.indexOf("r.") != 0)
- return handleRealPadId();
-
- exports.getPadId(padIdOrReadonlyPadId, function (err, value) {
- if(ERR(err, callback)) return;
- if (value == null)
- return handleRealPadId();
- callback(null, {
- readOnlyPadId: padIdOrReadonlyPadId,
- padId: value,
- readonly: true
- });
- });
}
diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js
index 4289e39c..6388f096 100644
--- a/src/node/db/SecurityManager.js
+++ b/src/node/db/SecurityManager.js
@@ -26,7 +26,8 @@ var authorManager = require("./AuthorManager");
var padManager = require("./PadManager");
var sessionManager = require("./SessionManager");
var settings = require("../utils/Settings");
-var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
+var log4js = require('log4js');
+var authLogger = log4js.getLogger("auth");
/**
* This function controlls the access to a pad, it checks if the user can access a pad.
@@ -39,6 +40,11 @@ var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
exports.checkAccess = function (padID, sessionCookie, token, password, callback)
{
var statusObject;
+
+ if(!padID) {
+ callback(null, {accessStatus: "deny"});
+ return;
+ }
// a valid session is required (api-only mode)
if(settings.requireSession)
@@ -117,31 +123,43 @@ exports.checkAccess = function (padID, sessionCookie, token, password, callback)
//get information about all sessions contained in this cookie
function(callback)
{
- if (!sessionCookie) {
+ if (!sessionCookie)
+ {
callback();
return;
}
var sessionIDs = sessionCookie.split(',');
- async.forEach(sessionIDs, function(sessionID, callback) {
- sessionManager.getSessionInfo(sessionID, function(err, sessionInfo) {
+ async.forEach(sessionIDs, function(sessionID, callback)
+ {
+ sessionManager.getSessionInfo(sessionID, function(err, sessionInfo)
+ {
//skip session if it doesn't exist
- if(err && err.message == "sessionID does not exist") return;
+ if(err && err.message == "sessionID does not exist")
+ {
+ authLogger.debug("Auth failed: unknown session");
+ callback();
+ return;
+ }
if(ERR(err, callback)) return;
var now = Math.floor(new Date().getTime()/1000);
//is it for this group?
- if(sessionInfo.groupID != groupID) {
- callback();
- return;
+ if(sessionInfo.groupID != groupID)
+ {
+ authLogger.debug("Auth failed: wrong group");
+ callback();
+ return;
}
//is validUntil still ok?
- if(sessionInfo.validUntil <= now){
- callback();
- return;
+ if(sessionInfo.validUntil <= now)
+ {
+ authLogger.debug("Auth failed: validUntil");
+ callback();
+ return;
}
// There is a valid session
@@ -234,7 +252,11 @@ exports.checkAccess = function (padID, sessionCookie, token, password, callback)
//--> grant access
statusObject = {accessStatus: "grant", authorID: sessionAuthor};
//--> deny access if user isn't allowed to create the pad
- if(settings.editOnly) statusObject.accessStatus = "deny";
+ if(settings.editOnly)
+ {
+ authLogger.debug("Auth failed: valid session & pad does not exist");
+ statusObject.accessStatus = "deny";
+ }
}
// there is no valid session avaiable AND pad exists
else if(!validSession && padExists)
@@ -266,6 +288,7 @@ exports.checkAccess = function (padID, sessionCookie, token, password, callback)
//- its not public
else if(!isPublic)
{
+ authLogger.debug("Auth failed: invalid session & pad is not public");
//--> deny access
statusObject = {accessStatus: "deny"};
}
@@ -277,6 +300,7 @@ exports.checkAccess = function (padID, sessionCookie, token, password, callback)
// there is no valid session avaiable AND pad doesn't exists
else
{
+ authLogger.debug("Auth failed: invalid session & pad does not exist");
//--> deny access
statusObject = {accessStatus: "deny"};
}
diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js
index 60e0a7ac..71315adc 100644
--- a/src/node/db/SessionManager.js
+++ b/src/node/db/SessionManager.js
@@ -21,7 +21,7 @@
var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
-var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
+var randomString = require("../utils/randomstring");
var db = require("./DB").db;
var async = require("async");
var groupMangager = require("./GroupManager");
@@ -263,12 +263,16 @@ exports.deleteSession = function(sessionID, callback)
db.remove("session:" + sessionID);
//remove session from group2sessions
- delete group2sessions.sessionIDs[sessionID];
- db.set("group2sessions:" + groupID, group2sessions);
-
+ if(group2sessions != null) { // Maybe the group was already deleted
+ delete group2sessions.sessionIDs[sessionID];
+ db.set("group2sessions:" + groupID, group2sessions);
+ }
+
//remove session from author2sessions
- delete author2sessions.sessionIDs[sessionID];
- db.set("author2sessions:" + authorID, author2sessions);
+ if(author2sessions != null) { // Maybe the author was already deleted
+ delete author2sessions.sessionIDs[sessionID];
+ db.set("author2sessions:" + authorID, author2sessions);
+ }
callback();
}
diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js
index 09ea7333..52a504f1 100644
--- a/src/node/db/SessionStore.js
+++ b/src/node/db/SessionStore.js
@@ -22,7 +22,7 @@ SessionStore.prototype.get = function(sid, fn){
{
if (sess) {
sess.cookie.expires = 'string' == typeof sess.cookie.expires ? new Date(sess.cookie.expires) : sess.cookie.expires;
- if (!sess.cookie.expires || new Date() < expires) {
+ if (!sess.cookie.expires || new Date() < sess.cookie.expires) {
fn(null, sess);
} else {
self.destroy(sid, fn);
diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js
index 4b7dd951..273a58a6 100644
--- a/src/node/handler/APIHandler.js
+++ b/src/node/handler/APIHandler.js
@@ -23,7 +23,7 @@ var ERR = require("async-stacktrace");
var fs = require("fs");
var api = require("../db/API");
var padManager = require("../db/PadManager");
-var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
+var randomString = require("../utils/randomstring");
//ensure we have an apikey
var apikey = null;
@@ -31,7 +31,7 @@ try
{
apikey = fs.readFileSync("./APIKEY.txt","utf8");
}
-catch(e)
+catch(e)
{
apikey = randomString(32);
fs.writeFileSync("./APIKEY.txt",apikey,"utf8");
@@ -180,7 +180,7 @@ var version =
, "deleteGroup" : ["groupID"]
, "listPads" : ["groupID"]
, "listAllPads" : []
- , "createDiffHTML" : ["padID", "startRev", "endRev"]
+ , "createDiffHTML" : ["padID", "startRev", "endRev"]
, "createPad" : ["padID", "text"]
, "createGroupPad" : ["groupID", "padName", "text"]
, "createAuthor" : ["name"]
@@ -214,10 +214,141 @@ var version =
, "getChatHistory" : ["padID", "start", "end"]
, "getChatHead" : ["padID"]
}
+, "1.2.8":
+ { "createGroup" : []
+ , "createGroupIfNotExistsFor" : ["groupMapper"]
+ , "deleteGroup" : ["groupID"]
+ , "listPads" : ["groupID"]
+ , "listAllPads" : []
+ , "createDiffHTML" : ["padID", "startRev", "endRev"]
+ , "createPad" : ["padID", "text"]
+ , "createGroupPad" : ["groupID", "padName", "text"]
+ , "createAuthor" : ["name"]
+ , "createAuthorIfNotExistsFor": ["authorMapper" , "name"]
+ , "listPadsOfAuthor" : ["authorID"]
+ , "createSession" : ["groupID", "authorID", "validUntil"]
+ , "deleteSession" : ["sessionID"]
+ , "getSessionInfo" : ["sessionID"]
+ , "listSessionsOfGroup" : ["groupID"]
+ , "listSessionsOfAuthor" : ["authorID"]
+ , "getText" : ["padID", "rev"]
+ , "setText" : ["padID", "text"]
+ , "getHTML" : ["padID", "rev"]
+ , "setHTML" : ["padID", "html"]
+ , "getAttributePool" : ["padID"]
+ , "getRevisionsCount" : ["padID"]
+ , "getRevisionChangeset" : ["padID", "rev"]
+ , "getLastEdited" : ["padID"]
+ , "deletePad" : ["padID"]
+ , "getReadOnlyID" : ["padID"]
+ , "setPublicStatus" : ["padID", "publicStatus"]
+ , "getPublicStatus" : ["padID"]
+ , "setPassword" : ["padID", "password"]
+ , "isPasswordProtected" : ["padID"]
+ , "listAuthorsOfPad" : ["padID"]
+ , "padUsersCount" : ["padID"]
+ , "getAuthorName" : ["authorID"]
+ , "padUsers" : ["padID"]
+ , "sendClientsMessage" : ["padID", "msg"]
+ , "listAllGroups" : []
+ , "checkToken" : []
+ , "getChatHistory" : ["padID"]
+ , "getChatHistory" : ["padID", "start", "end"]
+ , "getChatHead" : ["padID"]
+ }
+, "1.2.9":
+ { "createGroup" : []
+ , "createGroupIfNotExistsFor" : ["groupMapper"]
+ , "deleteGroup" : ["groupID"]
+ , "listPads" : ["groupID"]
+ , "listAllPads" : []
+ , "createDiffHTML" : ["padID", "startRev", "endRev"]
+ , "createPad" : ["padID", "text"]
+ , "createGroupPad" : ["groupID", "padName", "text"]
+ , "createAuthor" : ["name"]
+ , "createAuthorIfNotExistsFor": ["authorMapper" , "name"]
+ , "listPadsOfAuthor" : ["authorID"]
+ , "createSession" : ["groupID", "authorID", "validUntil"]
+ , "deleteSession" : ["sessionID"]
+ , "getSessionInfo" : ["sessionID"]
+ , "listSessionsOfGroup" : ["groupID"]
+ , "listSessionsOfAuthor" : ["authorID"]
+ , "getText" : ["padID", "rev"]
+ , "setText" : ["padID", "text"]
+ , "getHTML" : ["padID", "rev"]
+ , "setHTML" : ["padID", "html"]
+ , "getAttributePool" : ["padID"]
+ , "getRevisionsCount" : ["padID"]
+ , "getRevisionChangeset" : ["padID", "rev"]
+ , "getLastEdited" : ["padID"]
+ , "deletePad" : ["padID"]
+ , "copyPad" : ["sourceID", "destinationID", "force"]
+ , "movePad" : ["sourceID", "destinationID", "force"]
+ , "getReadOnlyID" : ["padID"]
+ , "setPublicStatus" : ["padID", "publicStatus"]
+ , "getPublicStatus" : ["padID"]
+ , "setPassword" : ["padID", "password"]
+ , "isPasswordProtected" : ["padID"]
+ , "listAuthorsOfPad" : ["padID"]
+ , "padUsersCount" : ["padID"]
+ , "getAuthorName" : ["authorID"]
+ , "padUsers" : ["padID"]
+ , "sendClientsMessage" : ["padID", "msg"]
+ , "listAllGroups" : []
+ , "checkToken" : []
+ , "getChatHistory" : ["padID"]
+ , "getChatHistory" : ["padID", "start", "end"]
+ , "getChatHead" : ["padID"]
+ }
+, "1.2.10":
+ { "createGroup" : []
+ , "createGroupIfNotExistsFor" : ["groupMapper"]
+ , "deleteGroup" : ["groupID"]
+ , "listPads" : ["groupID"]
+ , "listAllPads" : []
+ , "createDiffHTML" : ["padID", "startRev", "endRev"]
+ , "createPad" : ["padID", "text"]
+ , "createGroupPad" : ["groupID", "padName", "text"]
+ , "createAuthor" : ["name"]
+ , "createAuthorIfNotExistsFor": ["authorMapper" , "name"]
+ , "listPadsOfAuthor" : ["authorID"]
+ , "createSession" : ["groupID", "authorID", "validUntil"]
+ , "deleteSession" : ["sessionID"]
+ , "getSessionInfo" : ["sessionID"]
+ , "listSessionsOfGroup" : ["groupID"]
+ , "listSessionsOfAuthor" : ["authorID"]
+ , "getText" : ["padID", "rev"]
+ , "setText" : ["padID", "text"]
+ , "getHTML" : ["padID", "rev"]
+ , "setHTML" : ["padID", "html"]
+ , "getAttributePool" : ["padID"]
+ , "getRevisionsCount" : ["padID"]
+ , "getRevisionChangeset" : ["padID", "rev"]
+ , "getLastEdited" : ["padID"]
+ , "deletePad" : ["padID"]
+ , "copyPad" : ["sourceID", "destinationID", "force"]
+ , "movePad" : ["sourceID", "destinationID", "force"]
+ , "getReadOnlyID" : ["padID"]
+ , "getPadID" : ["roID"]
+ , "setPublicStatus" : ["padID", "publicStatus"]
+ , "getPublicStatus" : ["padID"]
+ , "setPassword" : ["padID", "password"]
+ , "isPasswordProtected" : ["padID"]
+ , "listAuthorsOfPad" : ["padID"]
+ , "padUsersCount" : ["padID"]
+ , "getAuthorName" : ["authorID"]
+ , "padUsers" : ["padID"]
+ , "sendClientsMessage" : ["padID", "msg"]
+ , "listAllGroups" : []
+ , "checkToken" : []
+ , "getChatHistory" : ["padID"]
+ , "getChatHistory" : ["padID", "start", "end"]
+ , "getChatHead" : ["padID"]
+ }
};
// set the latest available API version here
-exports.latestApiVersion = '1.2.7';
+exports.latestApiVersion = '1.2.10';
// exports the versions so it can be used by the new Swagger endpoint
exports.version = version;
@@ -241,7 +372,7 @@ exports.handle = function(apiVersion, functionName, fields, req, res)
break;
}
}
-
+
//say goodbye if this is an unkown API version
if(!isKnownApiVersion)
{
@@ -249,7 +380,7 @@ exports.handle = function(apiVersion, functionName, fields, req, res)
res.send({code: 3, message: "no such api version", data: null});
return;
}
-
+
//check if this is a valid function name
var isKnownFunctionname = false;
for(var knownFunctionname in version[apiVersion])
@@ -260,17 +391,17 @@ exports.handle = function(apiVersion, functionName, fields, req, res)
break;
}
}
-
+
//say goodbye if this is a unkown function
if(!isKnownFunctionname)
{
res.send({code: 3, message: "no such function", data: null});
return;
}
-
+
//check the api key!
fields["apikey"] = fields["apikey"] || fields["api_key"];
-
+
if(fields["apikey"] != apikey.trim())
{
res.send({code: 4, message: "no or wrong API Key", data: null});
@@ -304,21 +435,19 @@ exports.handle = function(apiVersion, functionName, fields, req, res)
function callAPI(apiVersion, functionName, fields, req, res)
{
//put the function parameters in an array
- var functionParams = [];
- for(var i=0;i<version[apiVersion][functionName].length;i++)
- {
- functionParams.push(fields[ version[apiVersion][functionName][i] ]);
- }
-
+ var functionParams = version[apiVersion][functionName].map(function (field) {
+ return fields[field]
+ })
+
//add a callback function to handle the response
functionParams.push(function(err, data)
- {
+ {
// no error happend, everything is fine
if(err == null)
{
if(!data)
data = null;
-
+
res.send({code: 0, message: "ok", data: data});
}
// parameters were wrong and the api stopped execution, pass the error
@@ -333,7 +462,7 @@ function callAPI(apiVersion, functionName, fields, req, res)
ERR(err);
}
});
-
+
//call the api function
- api[functionName](functionParams[0],functionParams[1],functionParams[2],functionParams[3],functionParams[4]);
+ api[functionName].apply(this, functionParams);
}
diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js
index 8ff5bc48..a748d3f2 100644
--- a/src/node/handler/ExportHandler.js
+++ b/src/node/handler/ExportHandler.js
@@ -27,6 +27,7 @@ var async = require("async");
var fs = require("fs");
var settings = require('../utils/Settings');
var os = require('os');
+var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
//load abiword only if its enabled
if(settings.abiword != null)
@@ -45,175 +46,186 @@ if(os.type().indexOf("Windows") > -1)
*/
exports.doExport = function(req, res, padId, type)
{
- //tell the browser that this is a downloadable file
- res.attachment(padId + "." + type);
+ var fileName = padId;
- //if this is a plain text export, we can do this directly
- // We have to over engineer this because tabs are stored as attributes and not plain text
+ // allow fileName to be overwritten by a hook, the type type is kept static for security reasons
+ hooks.aCallFirst("exportFileName", padId,
+ function(err, hookFileName){
+ // if fileName is set then set it to the padId, note that fileName is returned as an array.
+ if(hookFileName) fileName = hookFileName;
- if(type == "txt")
- {
- var txt;
- var randNum;
- var srcFile, destFile;
- async.series([
- //render the txt document
- function(callback)
+ //tell the browser that this is a downloadable file
+ res.attachment(fileName + "." + type);
+
+ //if this is a plain text export, we can do this directly
+ // We have to over engineer this because tabs are stored as attributes and not plain text
+
+ if(type == "txt")
{
- exporttxt.getPadTXTDocument(padId, req.params.rev, false, function(err, _txt)
- {
- if(ERR(err, callback)) return;
- txt = _txt;
- callback();
- });
- },
- //decide what to do with the txt export
- function(callback)
- {
- //if this is a txt export, we can send this from here directly
- res.send(txt);
- callback("stop");
- },
- //send the convert job to abiword
- function(callback)
- {
- //ensure html can be collected by the garbage collector
- txt = null;
-
- destFile = tempDirectory + "/eplite_export_" + randNum + "." + type;
- abiword.convertFile(srcFile, destFile, type, callback);
- },
- //send the file
- function(callback)
- {
- res.sendfile(destFile, null, callback);
- },
- //clean up temporary files
- function(callback)
- {
- async.parallel([
+ var txt;
+ var randNum;
+ var srcFile, destFile;
+
+ async.series([
+ //render the txt document
function(callback)
{
- fs.unlink(srcFile, callback);
+ exporttxt.getPadTXTDocument(padId, req.params.rev, false, function(err, _txt)
+ {
+ if(ERR(err, callback)) return;
+ txt = _txt;
+ callback();
+ });
},
+ //decide what to do with the txt export
function(callback)
{
- //100ms delay to accomidate for slow windows fs
- if(os.type().indexOf("Windows") > -1)
- {
- setTimeout(function()
+ //if this is a txt export, we can send this from here directly
+ res.send(txt);
+ callback("stop");
+ },
+ //send the convert job to abiword
+ function(callback)
+ {
+ //ensure html can be collected by the garbage collector
+ txt = null;
+
+ destFile = tempDirectory + "/etherpad_export_" + randNum + "." + type;
+ abiword.convertFile(srcFile, destFile, type, callback);
+ },
+ //send the file
+ function(callback)
+ {
+ res.sendfile(destFile, null, callback);
+ },
+ //clean up temporary files
+ function(callback)
+ {
+ async.parallel([
+ function(callback)
{
- fs.unlink(destFile, callback);
- }, 100);
- }
- else
- {
- fs.unlink(destFile, callback);
- }
+ fs.unlink(srcFile, callback);
+ },
+ function(callback)
+ {
+ //100ms delay to accomidate for slow windows fs
+ if(os.type().indexOf("Windows") > -1)
+ {
+ setTimeout(function()
+ {
+ fs.unlink(destFile, callback);
+ }, 100);
+ }
+ else
+ {
+ fs.unlink(destFile, callback);
+ }
+ }
+ ], callback);
}
- ], callback);
+ ], function(err)
+ {
+ if(err && err != "stop") ERR(err);
+ })
}
- ], function(err)
- {
- if(err && err != "stop") ERR(err);
- })
- }
- else if(type == 'dokuwiki')
- {
- var randNum;
- var srcFile, destFile;
-
- async.series([
- //render the dokuwiki document
- function(callback)
+ else if(type == 'dokuwiki')
{
- exportdokuwiki.getPadDokuWikiDocument(padId, req.params.rev, function(err, dokuwiki)
+ var randNum;
+ var srcFile, destFile;
+
+ async.series([
+ //render the dokuwiki document
+ function(callback)
+ {
+ exportdokuwiki.getPadDokuWikiDocument(padId, req.params.rev, function(err, dokuwiki)
+ {
+ res.send(dokuwiki);
+ callback("stop");
+ });
+ },
+ ], function(err)
{
- res.send(dokuwiki);
- callback("stop");
+ if(err && err != "stop") throw err;
});
- },
- ], function(err)
- {
- if(err && err != "stop") throw err;
- });
- }
- else
- {
- var html;
- var randNum;
- var srcFile, destFile;
-
- async.series([
- //render the html document
- function(callback)
- {
- exporthtml.getPadHTMLDocument(padId, req.params.rev, false, function(err, _html)
- {
- if(ERR(err, callback)) return;
- html = _html;
- callback();
- });
- },
- //decide what to do with the html export
- function(callback)
- {
- //if this is a html export, we can send this from here directly
- if(type == "html")
- {
- res.send(html);
- callback("stop");
- }
- else //write the html export to a file
- {
- randNum = Math.floor(Math.random()*0xFFFFFFFF);
- srcFile = tempDirectory + "/eplite_export_" + randNum + ".html";
- fs.writeFile(srcFile, html, callback);
- }
- },
- //send the convert job to abiword
- function(callback)
- {
- //ensure html can be collected by the garbage collector
- html = null;
-
- destFile = tempDirectory + "/eplite_export_" + randNum + "." + type;
- abiword.convertFile(srcFile, destFile, type, callback);
- },
- //send the file
- function(callback)
- {
- res.sendfile(destFile, null, callback);
- },
- //clean up temporary files
- function(callback)
+ }
+ else
{
- async.parallel([
+ var html;
+ var randNum;
+ var srcFile, destFile;
+
+ async.series([
+ //render the html document
function(callback)
{
- fs.unlink(srcFile, callback);
+ exporthtml.getPadHTMLDocument(padId, req.params.rev, false, function(err, _html)
+ {
+ if(ERR(err, callback)) return;
+ html = _html;
+ callback();
+ });
},
+ //decide what to do with the html export
function(callback)
{
- //100ms delay to accomidate for slow windows fs
- if(os.type().indexOf("Windows") > -1)
+ //if this is a html export, we can send this from here directly
+ if(type == "html")
{
- setTimeout(function()
- {
- fs.unlink(destFile, callback);
- }, 100);
+ res.send(html);
+ callback("stop");
}
- else
+ else //write the html export to a file
{
- fs.unlink(destFile, callback);
+ randNum = Math.floor(Math.random()*0xFFFFFFFF);
+ srcFile = tempDirectory + "/etherpad_export_" + randNum + ".html";
+ fs.writeFile(srcFile, html, callback);
}
+ },
+ //send the convert job to abiword
+ function(callback)
+ {
+ //ensure html can be collected by the garbage collector
+ html = null;
+
+ destFile = tempDirectory + "/etherpad_export_" + randNum + "." + type;
+ abiword.convertFile(srcFile, destFile, type, callback);
+ },
+ //send the file
+ function(callback)
+ {
+ res.sendfile(destFile, null, callback);
+ },
+ //clean up temporary files
+ function(callback)
+ {
+ async.parallel([
+ function(callback)
+ {
+ fs.unlink(srcFile, callback);
+ },
+ function(callback)
+ {
+ //100ms delay to accomidate for slow windows fs
+ if(os.type().indexOf("Windows") > -1)
+ {
+ setTimeout(function()
+ {
+ fs.unlink(destFile, callback);
+ }, 100);
+ }
+ else
+ {
+ fs.unlink(destFile, callback);
+ }
+ }
+ ], callback);
}
- ], callback);
+ ], function(err)
+ {
+ if(err && err != "stop") ERR(err);
+ })
}
- ], function(err)
- {
- if(err && err != "stop") ERR(err);
- })
- }
+ }
+ );
};
diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js
index ac856a60..60fa5ffb 100644
--- a/src/node/handler/ImportHandler.js
+++ b/src/node/handler/ImportHandler.js
@@ -28,7 +28,9 @@ var ERR = require("async-stacktrace")
, settings = require('../utils/Settings')
, formidable = require('formidable')
, os = require("os")
- , importHtml = require("../utils/ImportHtml");
+ , importHtml = require("../utils/ImportHtml")
+ , log4js = require("log4js")
+ , hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js");
//load abiword only if its enabled
if(settings.abiword != null)
@@ -42,13 +44,18 @@ var tmpDirectory = process.env.TEMP || process.env.TMPDIR || process.env.TMP ||
*/
exports.doImport = function(req, res, padId)
{
+ var apiLogger = log4js.getLogger("ImportHandler");
+
//pipe to a file
//convert file to html via abiword
//set html in the pad
var srcFile, destFile
, pad
- , text;
+ , text
+ , importHandledByPlugin;
+
+ var randNum = Math.floor(Math.random()*0xFFFFFFFF);
async.series([
//save the uploaded file to /tmp
@@ -60,7 +67,7 @@ exports.doImport = function(req, res, padId)
form.parse(req, function(err, fields, files) {
//the upload failed, stop at this point
if(err || files.file === undefined) {
- console.warn("Uploading Error: " + err.stack);
+ if(err) console.warn("Uploading Error: " + err.stack);
callback("uploadFailed");
}
//everything ok, continue
@@ -87,32 +94,71 @@ exports.doImport = function(req, res, padId)
else {
var oldSrcFile = srcFile;
srcFile = path.join(path.dirname(srcFile),path.basename(srcFile, fileEnding)+".txt");
-
fs.rename(oldSrcFile, srcFile, callback);
}
},
-
+ function(callback){
+ destFile = path.join(tmpDirectory, "etherpad_import_" + randNum + ".htm");
+
+ // Logic for allowing external Import Plugins
+ hooks.aCallAll("import", {srcFile: srcFile, destFile: destFile}, function(err, result){
+ if(ERR(err, callback)) return callback();
+ if(result.length > 0){ // This feels hacky and wrong..
+ importHandledByPlugin = true;
+ callback();
+ }else{
+ callback();
+ }
+ });
+ },
//convert file to html
function(callback) {
- var randNum = Math.floor(Math.random()*0xFFFFFFFF);
- destFile = path.join(tmpDirectory, "eplite_import_" + randNum + ".htm");
-
- if (abiword) {
- abiword.convertFile(srcFile, destFile, "htm", function(err) {
- //catch convert errors
- if(err) {
- console.warn("Converting Error:", err);
- return callback("convertFailed");
- } else {
+ if(!importHandledByPlugin){
+ var fileEnding = path.extname(srcFile).toLowerCase();
+ var fileIsHTML = (fileEnding === ".html" || fileEnding === ".htm");
+ if (abiword && !fileIsHTML) {
+ abiword.convertFile(srcFile, destFile, "htm", function(err) {
+ //catch convert errors
+ if(err) {
+ console.warn("Converting Error:", err);
+ return callback("convertFailed");
+ } else {
+ callback();
+ }
+ });
+ } else {
+ // if no abiword only rename
+ fs.rename(srcFile, destFile, callback);
+ }
+ }else{
+ callback();
+ }
+ },
+
+ function(callback) {
+ if (!abiword) {
+ // Read the file with no encoding for raw buffer access.
+ fs.readFile(destFile, function(err, buf) {
+ if (err) throw err;
+ var isAscii = true;
+ // Check if there are only ascii chars in the uploaded file
+ for (var i=0, len=buf.length; i<len; i++) {
+ if (buf[i] > 240) {
+ isAscii=false;
+ break;
+ }
+ }
+ if (isAscii) {
callback();
+ } else {
+ callback("uploadFailed");
}
});
} else {
- // if no abiword only rename
- fs.rename(srcFile, destFile, callback);
+ callback();
}
},
-
+
//get the pad object
function(callback) {
padManager.getPad(padId, function(err, _pad){
@@ -127,7 +173,10 @@ exports.doImport = function(req, res, padId)
fs.readFile(destFile, "utf8", function(err, _text){
if(ERR(err, callback)) return;
text = _text;
-
+ // Title needs to be stripped out else it appends it to the pad..
+ text = text.replace("<title>", "<!-- <title>");
+ text = text.replace("</title>","</title>-->");
+
//node on windows has a delay on releasing of the file lock.
//We add a 100ms delay to work around this
if(os.type().indexOf("Windows") > -1){
@@ -142,7 +191,11 @@ exports.doImport = function(req, res, padId)
function(callback) {
var fileEnding = path.extname(srcFile).toLowerCase();
if (abiword || fileEnding == ".htm" || fileEnding == ".html") {
- importHtml.setPadHTML(pad, text);
+ try{
+ importHtml.setPadHTML(pad, text);
+ }catch(e){
+ apiLogger.warn("Error importing, possibly caused by malformed HTML");
+ }
} else {
pad.setText(text);
}
@@ -176,7 +229,7 @@ exports.doImport = function(req, res, padId)
ERR(err);
//close the connection
- res.send("<head><script type='text/javascript' src='../../static/js/jquery.js'></script></head><script>$(window).load(function(){if ( (!$.browser.msie) && (!($.browser.mozilla && $.browser.version.indexOf(\"1.8.\") == 0)) ){document.domain = document.domain;}var impexp = window.parent.padimpexp.handleFrameCall('" + status + "');})</script>", 200);
+ res.send("<head><script type='text/javascript' src='../../static/js/jquery.js'></script><script type='text/javascript' src='../../static/js/jquery_browser.js'></script></head><script>$(window).load(function(){if ( (!$.browser.msie) && (!($.browser.mozilla && $.browser.version.indexOf(\"1.8.\") == 0)) ){document.domain = document.domain;}var impexp = window.parent.padimpexp.handleFrameCall('" + status + "');})</script>", 200);
});
}
diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js
index 35f1ab4c..26eb17a6 100644
--- a/src/node/handler/PadMessageHandler.js
+++ b/src/node/handler/PadMessageHandler.js
@@ -1,6 +1,6 @@
/**
- * The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions
- */
+ * The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions
+ */
/*
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
@@ -35,6 +35,8 @@ var messageLogger = log4js.getLogger("message");
var accessLogger = log4js.getLogger("access");
var _ = require('underscore');
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js");
+var channels = require("channels");
+var stats = require('../stats');
/**
* A associative array that saves informations about a session
@@ -47,6 +49,17 @@ var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js");
* author = the author name of this session
*/
var sessioninfos = {};
+exports.sessioninfos = sessioninfos;
+
+// Measure total amount of users
+stats.gauge('totalUsers', function() {
+ return Object.keys(socketio.sockets.sockets).length
+})
+
+/**
+ * A changeset queue per pad that is processed by handleUserChanges()
+ */
+var padChannels = new channels.channels(handleUserChanges);
/**
* Saves the Socket class we need to send and recieve data from the client
@@ -67,7 +80,9 @@ exports.setSocketIO = function(socket_io)
* @param client the new client
*/
exports.handleConnect = function(client)
-{
+{
+ stats.meter('connects').mark();
+
//Initalize sessioninfos for this new session
sessioninfos[client.id]={};
}
@@ -91,18 +106,29 @@ exports.kickSessionsFromPad = function(padID)
* @param client the client that leaves
*/
exports.handleDisconnect = function(client)
-{
+{
+ stats.meter('disconnects').mark();
+
//save the padname of this session
var session = sessioninfos[client.id];
-
+
//if this connection was already etablished with a handshake, send a disconnect message to the others
if(session && session.author)
{
+ client.get('remoteAddress', function(er, ip) {
+ //Anonymize the IP address if IP logging is disabled
+ if(settings.disableIPlogging) {
+ ip = 'ANONYMOUS';
+ }
+
+ accessLogger.info('[LEAVE] Pad "'+session.padId+'": Author "'+session.author+'" on client '+client.id+' with IP "'+ip+'" left the pad')
+ })
+
//get the author color out of the db
authorManager.getAuthorColorId(session.author, function(err, color)
{
ERR(err);
-
+
//prepare the notification for the other users on the pad, that this user left
var messageToTheOtherUsers = {
"type": "COLLABROOM",
@@ -116,18 +142,14 @@ exports.handleDisconnect = function(client)
}
}
};
-
+
//Go trough all user that are still on the pad, and send them the USER_LEAVE message
client.broadcast.to(session.padId).json.send(messageToTheOtherUsers);
- });
+ });
}
-
- client.get('remoteAddress', function(er, ip) {
- accessLogger.info('[LEAVE] Pad "'+session.padId+'": Author "'+session.author+'" on client '+client.id+' with IP "'+ip+'" left the pad')
- })
-
+
//Delete the sessioninfos entrys of this session
- delete sessioninfos[client.id];
+ delete sessioninfos[client.id];
}
/**
@@ -136,33 +158,33 @@ exports.handleDisconnect = function(client)
* @param message the message from the client
*/
exports.handleMessage = function(client, message)
-{
-
+{
if(message == null)
{
- messageLogger.warn("Message is null!");
return;
}
if(!message.type)
{
- messageLogger.warn("Message has no type attribute!");
+ return;
+ }
+ var thisSession = sessioninfos[client.id]
+ if(!thisSession) {
+ messageLogger.warn("Dropped message from an unknown connection.")
return;
}
var handleMessageHook = function(callback){
var dropMessage = false;
-
- // Call handleMessage hook. If a plugin returns null, the message will be dropped. Note that for all messages
+ // Call handleMessage hook. If a plugin returns null, the message will be dropped. Note that for all messages
// handleMessage will be called, even if the client is not authorized
hooks.aCallAll("handleMessage", { client: client, message: message }, function ( err, messages ) {
if(ERR(err, callback)) return;
-
_.each(messages, function(newMessage){
if ( newMessage === null ) {
dropMessage = true;
}
});
-
+
// If no plugins explicitly told us to drop the message, its ok to proceed
if(!dropMessage){ callback() };
});
@@ -175,10 +197,11 @@ exports.handleMessage = function(client, message)
} else if(message.type == "CHANGESET_REQ") {
handleChangesetRequest(client, message);
} else if(message.type == "COLLABROOM") {
- if (sessioninfos[client.id].readonly) {
+ if (thisSession.readonly) {
messageLogger.warn("Dropped message, COLLABROOM for readonly pad");
} else if (message.data.type == "USER_CHANGES") {
- handleUserChanges(client, message);
+ stats.counter('pendingEdits').inc()
+ padChannels.emit(message.padId, {client: client, message: message});// add to pad queue
} else if (message.data.type == "USERINFO_UPDATE") {
handleUserInfoUpdate(client, message);
} else if (message.data.type == "CHAT_MESSAGE") {
@@ -205,31 +228,61 @@ exports.handleMessage = function(client, message)
//check permissions
function(callback)
{
-
- // If the message has a padId we assume the client is already known to the server and needs no re-authorization
- if(!message.padId)
- return callback();
+ // client tried to auth for the first time (first msg from the client)
+ if(message.type == "CLIENT_READY") {
+ // Remember this information since we won't
+ // have the cookie in further socket.io messages.
+ // This information will be used to check if
+ // the sessionId of this connection is still valid
+ // since it could have been deleted by the API.
+ sessioninfos[client.id].auth =
+ {
+ sessionID: message.sessionID,
+ padID: message.padId,
+ token : message.token,
+ password: message.password
+ };
+ }
// Note: message.sessionID is an entirely different kind of
- // session from the sessions we use here! Beware! FIXME: Call
- // our "sessions" "connections".
+ // session from the sessions we use here! Beware!
+ // FIXME: Call our "sessions" "connections".
// FIXME: Use a hook instead
// FIXME: Allow to override readwrite access with readonly
- securityManager.checkAccess(message.padId, message.sessionID, message.token, message.password, function(err, statusObject)
- {
- if(ERR(err, callback)) return;
- //access was granted
- if(statusObject.accessStatus == "grant")
- {
- callback();
- }
- //no access, send the client a message that tell him why
- else
+ // FIXME: A message might arrive but wont have an auth object, this is obviously bad so we should deny it
+ // Simulate using the load testing tool
+ if(!sessioninfos[client.id].auth){
+ console.error("Auth was never applied to a session. If you are using the stress-test tool then restart Etherpad and the Stress test tool.")
+ callback();
+ }else{
+ var auth = sessioninfos[client.id].auth;
+ var checkAccessCallback = function(err, statusObject)
{
- client.json.send({accessStatus: statusObject.accessStatus})
+ if(ERR(err, callback)) return;
+
+ //access was granted
+ if(statusObject.accessStatus == "grant")
+ {
+ callback();
+ }
+ //no access, send the client a message that tell him why
+ else
+ {
+ client.json.send({accessStatus: statusObject.accessStatus})
+ }
+ };
+ //check if pad is requested via readOnly
+ if (auth.padID.indexOf("r.") === 0) {
+ //Pad is readOnly, first get the real Pad ID
+ readOnlyManager.getPadId(auth.padID, function(err, value) {
+ ERR(err);
+ securityManager.checkAccess(value, auth.sessionID, auth.token, auth.password, checkAccessCallback);
+ });
+ } else {
+ securityManager.checkAccess(auth.padID, auth.sessionID, auth.token, auth.password, checkAccessCallback);
}
- });
+ }
},
finalHandler
]);
@@ -245,16 +298,35 @@ exports.handleMessage = function(client, message)
function handleSaveRevisionMessage(client, message){
var padId = sessioninfos[client.id].padId;
var userId = sessioninfos[client.id].author;
-
+
padManager.getPad(padId, function(err, pad)
{
if(ERR(err)) return;
-
+
pad.addSavedRevision(pad.head, userId);
});
}
/**
+ * Handles a custom message, different to the function below as it handles objects not strings and you can
+ * direct the message to specific sessionID
+ *
+ * @param msg {Object} the message we're sending
+ * @param sessionID {string} the socketIO session to which we're sending this message
+ */
+exports.handleCustomObjectMessage = function (msg, sessionID, cb) {
+ if(msg.data.type === "CUSTOM"){
+ if(sessionID){ // If a sessionID is targeted then send directly to this sessionID
+ socketio.sockets.socket(sessionID).json.send(msg); // send a targeted message
+ }else{
+ socketio.sockets.in(msg.data.payload.padId).json.send(msg); // broadcast to all clients on this pad
+ }
+ }
+ cb(null, {});
+}
+
+
+/**
* Handles a custom message (sent via HTTP API request)
*
* @param padID {Pad} the pad to which we're sending this message
@@ -285,10 +357,10 @@ function handleChatMessage(client, message)
var userId = sessioninfos[client.id].author;
var text = message.data.text;
var padId = sessioninfos[client.id].padId;
-
+
var pad;
var userName;
-
+
async.series([
//get the pad
function(callback)
@@ -314,7 +386,7 @@ function handleChatMessage(client, message)
{
//save the chat message
pad.appendChatMessage(text, userId, time);
-
+
var msg = {
type: "COLLABROOM",
data: {
@@ -325,10 +397,10 @@ function handleChatMessage(client, message)
text: text
}
};
-
+
//broadcast the chat message to everyone on the pad
socketio.sockets.in(padId).json.send(msg);
-
+
callback();
}
], function(err)
@@ -354,20 +426,20 @@ function handleGetChatMessages(client, message)
messageLogger.warn("Dropped message, GetChatMessages Message has no start!");
return;
}
-
+
var start = message.data.start;
var end = message.data.end;
var count = start - count;
-
+
if(count < 0 && count > 100)
{
messageLogger.warn("Dropped message, GetChatMessages Message, client requested invalid amout of messages!");
return;
}
-
+
var padId = sessioninfos[client.id].padId;
var pad;
-
+
async.series([
//get the pad
function(callback)
@@ -384,7 +456,7 @@ function handleGetChatMessages(client, message)
pad.getChatMessages(start, end, function(err, chatMessages)
{
if(ERR(err, callback)) return;
-
+
var infoMsg = {
type: "COLLABROOM",
data: {
@@ -392,7 +464,7 @@ function handleGetChatMessages(client, message)
messages: chatMessages
}
};
-
+
// send the messages back to the client
client.json.send(infoMsg);
});
@@ -417,10 +489,10 @@ function handleSuggestUserName(client, message)
messageLogger.warn("Dropped message, suggestUserName Message has no unnamedId!");
return;
}
-
+
var padId = sessioninfos[client.id].padId,
clients = socketio.sockets.clients(padId);
-
+
//search the author and send him this message
for(var i = 0; i < clients.length; i++) {
var session = sessioninfos[clients[i].id];
@@ -449,14 +521,14 @@ function handleUserInfoUpdate(client, message)
messageLogger.warn("Dropped message, USERINFO_UPDATE Message has no colorId!");
return;
}
-
+
//Find out the author name of this session
var author = sessioninfos[client.id].author;
-
+
//Tell the authorManager about the new attributes
authorManager.setAuthorColorId(author, message.data.userInfo.colorId);
authorManager.setAuthorName(author, message.data.userInfo.name);
-
+
var padId = sessioninfos[client.id].padId;
var infoMsg = {
@@ -474,7 +546,7 @@ function handleUserInfoUpdate(client, message)
}
}
};
-
+
//Send the other clients on the pad the update message
client.broadcast.to(padId).json.send(infoMsg);
}
@@ -493,25 +565,39 @@ function handleUserInfoUpdate(client, message)
* @param client the client that send this message
* @param message the message from the client
*/
-function handleUserChanges(client, message)
+function handleUserChanges(data, cb)
{
+ var client = data.client
+ , message = data.message
+
+ // This one's no longer pending, as we're gonna process it now
+ stats.counter('pendingEdits').dec()
+
// Make sure all required fields are present
if(message.data.baseRev == null)
{
messageLogger.warn("Dropped message, USER_CHANGES Message has no baseRev!");
- return;
+ return cb();
}
if(message.data.apool == null)
{
messageLogger.warn("Dropped message, USER_CHANGES Message has no apool!");
- return;
+ return cb();
}
if(message.data.changeset == null)
{
messageLogger.warn("Dropped message, USER_CHANGES Message has no changeset!");
- return;
+ return cb();
}
-
+ //TODO: this might happen with other messages too => find one place to copy the session
+ //and always use the copy. atm a message will be ignored if the session is gone even
+ //if the session was valid when the message arrived in the first place
+ if(!sessioninfos[client.id])
+ {
+ messageLogger.warn("Dropped message, disconnect happened in the mean time");
+ return cb();
+ }
+
//get all Vars we need
var baseRev = message.data.baseRev;
var wireApool = (new AttributePool()).fromJsonable(message.data.apool);
@@ -519,9 +605,12 @@ function handleUserChanges(client, message)
// The client might disconnect between our callbacks. We should still
// finish processing the changeset, so keep a reference to the session.
var thisSession = sessioninfos[client.id];
-
+
var r, apool, pad;
-
+
+ // Measure time to process edit
+ var stopWatch = stats.timer('edits').start();
+
async.series([
//get the pad
function(callback)
@@ -537,7 +626,7 @@ function handleUserChanges(client, message)
function(callback)
{
//ex. _checkChangesetAndPool
-
+
try
{
// Verify that the changeset has valid syntax and is in canonical form
@@ -547,28 +636,37 @@ function handleUserChanges(client, message)
// defined in the accompanying attribute pool.
Changeset.eachAttribNumber(changeset, function(n) {
if (! wireApool.getAttrib(n)) {
- throw "Attribute pool is missing attribute "+n+" for changeset "+changeset;
+ throw new Error("Attribute pool is missing attribute "+n+" for changeset "+changeset);
}
});
- // Validate all 'author' attribs to be the same value as the current user
- wireApool.eachAttrib(function(type, value) {
- if('author' == type && value != thisSession.author) throw "Trying to submit changes as another author"
- })
+ // Validate all added 'author' attribs to be the same value as the current user
+ var iterator = Changeset.opIterator(Changeset.unpack(changeset).ops)
+ , op
+ while(iterator.hasNext()) {
+ op = iterator.next()
+ if(op.opcode != '+') continue;
+ op.attribs.split('*').forEach(function(attr) {
+ if(!attr) return
+ attr = wireApool.getAttrib(attr)
+ if(!attr) return
+ if('author' == attr[0] && attr[1] != thisSession.author) throw new Error("Trying to submit changes as another author in changeset "+changeset);
+ })
+ }
+
+ //ex. adoptChangesetAttribs
+
+ //Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
+ changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool);
}
catch(e)
{
// There is an error in this changeset, so just refuse it
- console.warn("Can't apply USER_CHANGES "+changeset+", because: "+e);
client.json.send({disconnect:"badChangeset"});
- return;
+ stats.meter('failedChangesets').mark();
+ return callback(new Error("Can't apply USER_CHANGES, because "+e.message));
}
-
- //ex. adoptChangesetAttribs
-
- //Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
- changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool);
-
+
//ex. applyUserChanges
apool = pad.pool;
r = baseRev;
@@ -582,7 +680,7 @@ function handleUserChanges(client, message)
function(callback)
{
r++;
-
+
pad.getRevisionChangeset(r, function(err, c)
{
if(ERR(err, callback)) return;
@@ -595,9 +693,9 @@ function handleUserChanges(client, message)
{
changeset = Changeset.follow(c, changeset, false, apool);
}catch(e){
- console.warn("Can't apply USER_CHANGES "+changeset+", possibly because of mismatched follow error");
client.json.send({disconnect:"badChangeset"});
- return;
+ stats.meter('failedChangesets').mark();
+ return callback(new Error("Can't apply USER_CHANGES, because "+e.message));
}
if ((r - baseRev) % 200 == 0) { // don't let the stack get too deep
@@ -615,17 +713,16 @@ function handleUserChanges(client, message)
function (callback)
{
var prevText = pad.text();
-
- if (Changeset.oldLen(changeset) != prevText.length)
+
+ if (Changeset.oldLen(changeset) != prevText.length)
{
- console.warn("Can't apply USER_CHANGES "+changeset+" with oldLen " + Changeset.oldLen(changeset) + " to document of length " + prevText.length);
client.json.send({disconnect:"badChangeset"});
- callback();
- return;
+ stats.meter('failedChangesets').mark();
+ return callback(new Error("Can't apply USER_CHANGES "+changeset+" with oldLen " + Changeset.oldLen(changeset) + " to document of length " + prevText.length));
}
-
+
pad.appendRevision(changeset, thisSession.author);
-
+
var correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool);
if (correctionChangeset) {
pad.appendRevision(correctionChangeset);
@@ -636,26 +733,31 @@ function handleUserChanges(client, message)
var nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length-1, 0, "\n");
pad.appendRevision(nlChangeset);
}
-
- exports.updatePadClients(pad, callback);
+
+ exports.updatePadClients(pad, function(er) {
+ ERR(er)
+ });
+ callback();
}
], function(err)
{
- ERR(err);
+ stopWatch.end()
+ cb();
+ if(err) console.warn(err.stack || err)
});
}
exports.updatePadClients = function(pad, callback)
-{
+{
//skip this step if noone is on this pad
var roomClients = socketio.sockets.clients(pad.id);
if(roomClients.length==0)
return callback();
-
+
// since all clients usually get the same set of changesets, store them in local cache
// to remove unnecessary roundtrip to the datalayer
// TODO: in REAL world, if we're working without datalayer cache, all requests to revisions will be fired
- // BEFORE first result will be landed to our cache object. The solution is to replace parallel processing
+ // BEFORE first result will be landed to our cache object. The solution is to replace parallel processing
// via async.forEach with sequential for() loop. There is no real benefits of running this in parallel,
// but benefit of reusing cached revision object is HUGE
var revCache = {};
@@ -670,7 +772,7 @@ exports.updatePadClients = function(pad, callback)
async.whilst(
function (){ return sessioninfos[sid] && sessioninfos[sid].rev < pad.getHeadRevisionNumber()},
function(callback)
- {
+ {
var r = sessioninfos[sid].rev + 1;
async.waterfall([
@@ -679,7 +781,7 @@ exports.updatePadClients = function(pad, callback)
callback(null, revCache[r]);
else
pad.getRevision(r, callback);
- },
+ },
function(revision, callback)
{
revCache[r] = revision;
@@ -707,8 +809,8 @@ exports.updatePadClients = function(pad, callback)
author: author,
currentTime: currentTime,
timeDelta: currentTime - sessioninfos[sid].time
- }};
-
+ }};
+
client.json.send(wireMsg);
}
@@ -721,11 +823,11 @@ exports.updatePadClients = function(pad, callback)
},
callback
);
- },callback);
+ },callback);
}
/**
- * Copied from the Etherpad Source Code. Don't know what this methode does excatly...
+ * Copied from the Etherpad Source Code. Don't know what this method does excatly...
*/
function _correctMarkersInPad(atext, apool) {
var text = atext.text;
@@ -737,11 +839,11 @@ function _correctMarkersInPad(atext, apool) {
var offset = 0;
while (iter.hasNext()) {
var op = iter.next();
-
+
var hasMarker = _.find(AttributeManager.lineAttributes, function(attribute){
return Changeset.opAttributeValue(op, attribute, apool);
}) !== undefined;
-
+
if (hasMarker) {
for(var i=0;i<op.chars;i++) {
if (offset > 0 && text.charAt(offset-1) != '\n') {
@@ -771,7 +873,7 @@ function _correctMarkersInPad(atext, apool) {
}
/**
- * Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token
+ * Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token
* and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad
* @param client the client that send this message
* @param message the message from the client
@@ -829,7 +931,7 @@ function handleClientReady(client, message)
securityManager.checkAccess (padIds.padId, message.sessionID, message.token, message.password, function(err, statusObject)
{
if(ERR(err, callback)) return;
-
+
//access was granted
if(statusObject.accessStatus == "grant")
{
@@ -842,7 +944,7 @@ function handleClientReady(client, message)
client.json.send({accessStatus: statusObject.accessStatus})
}
});
- },
+ },
//get all authordata of this new user, and load the pad-object from the database
function(callback)
{
@@ -874,7 +976,7 @@ function handleClientReady(client, message)
function(callback)
{
var authors = pad.getAllAuthors();
-
+
async.parallel([
//get timestamp of latest revission needed for timeslider
function(callback)
@@ -894,14 +996,13 @@ function handleClientReady(client, message)
authorManager.getAuthor(authorId, function(err, author)
{
if(ERR(err, callback)) return;
- delete author.timestamp;
- historicalAuthorData[authorId] = author;
+ historicalAuthorData[authorId] = {name: author.name, colorId: author.colorId}; // Filter author attribs (e.g. don't send author's pads to all clients)
callback();
});
}, callback);
}
], callback);
-
+
},
//glue the clientVars together, send them and tell the other clients that a new one is there
function(callback)
@@ -921,14 +1022,19 @@ function handleClientReady(client, message)
roomClients[i].json.send({disconnect:"userdup"});
}
}
-
+
//Save in sessioninfos that this session belonges to this pad
sessioninfos[client.id].padId = padIds.padId;
sessioninfos[client.id].readOnlyPadId = padIds.readOnlyPadId;
sessioninfos[client.id].readonly = padIds.readonly;
-
+
//Log creation/(re-)entering of a pad
client.get('remoteAddress', function(er, ip) {
+ //Anonymize the IP address if IP logging is disabled
+ if(settings.disableIPlogging) {
+ ip = 'ANONYMOUS';
+ }
+
if(pad.head > 0) {
accessLogger.info('[ENTER] Pad "'+padIds.padId+'": Client '+client.id+' with IP "'+ip+'" entered the pad');
}
@@ -948,12 +1054,18 @@ function handleClientReady(client, message)
//This is a normal first connect
else
{
- //prepare all values for the wire
- var atext = Changeset.cloneAText(pad.atext);
- var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
- var apool = attribsForWire.pool.toJsonable();
- atext.attribs = attribsForWire.translated;
-
+ //prepare all values for the wire, there'S a chance that this throws, if the pad is corrupted
+ try {
+ var atext = Changeset.cloneAText(pad.atext);
+ var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
+ var apool = attribsForWire.pool.toJsonable();
+ atext.attribs = attribsForWire.translated;
+ }catch(e) {
+ console.error(e.stack || e)
+ client.json.send({disconnect:"corruptPad"});// pull the breaks
+ return callback();
+ }
+
// Warning: never ever send padIds.padId to the client. If the
// client is read only you would open a security hole 1 swedish
// mile wide...
@@ -973,7 +1085,6 @@ function handleClientReady(client, message)
"historicalAuthorData": historicalAuthorData,
"apool": apool,
"rev": pad.getHeadRevisionNumber(),
- "globalPadId": message.padId,
"time": currentTime,
},
"colorPalette": authorManager.getColorPalette(),
@@ -983,16 +1094,15 @@ function handleClientReady(client, message)
"padId": message.padId,
"initialTitle": "Pad: " + message.padId,
"opts": {},
- // tell the client the number of the latest chat-message, which will be
+ // tell the client the number of the latest chat-message, which will be
// used to request the latest 100 chat-messages later (GET_CHAT_MESSAGES)
"chatHead": pad.chatHead,
"numConnectedUsers": roomClients.length,
"readOnlyId": padIds.readOnlyPadId,
"readonly": padIds.readonly,
"serverTimestamp": new Date().getTime(),
- "globalPadId": message.padId,
"userId": author,
- "abiwordAvailable": settings.abiwordAvailable(),
+ "abiwordAvailable": settings.abiwordAvailable(),
"plugins": {
"plugins": plugins.plugins,
"parts": plugins.parts,
@@ -1005,18 +1115,18 @@ function handleClientReady(client, message)
{
clientVars.userName = authorName;
}
-
+
//call the clientVars-hook so plugins can modify them before they get sent to the client
hooks.aCallAll("clientVars", { clientVars: clientVars, pad: pad }, function ( err, messages ) {
if(ERR(err, callback)) return;
-
+
_.each(messages, function(newVars) {
//combine our old object with the new attributes from the hook
for(var attr in newVars) {
clientVars[attr] = newVars[attr];
}
});
-
+
//Join the pad and start receiving updates
client.join(padIds.padId);
//Send the clientVars to the Client
@@ -1025,9 +1135,9 @@ function handleClientReady(client, message)
sessioninfos[client.id].rev = pad.getHeadRevisionNumber();
});
}
-
+
sessioninfos[client.id].author = author;
-
+
//prepare the notification for the other users on the pad, that this user joined
var messageToTheOtherUsers = {
"type": "COLLABROOM",
@@ -1041,7 +1151,7 @@ function handleClientReady(client, message)
}
}
};
-
+
//Add the authorname of this new User, if avaiable
if(authorName != null)
{
@@ -1050,7 +1160,7 @@ function handleClientReady(client, message)
// notify all existing users about new user
client.broadcast.to(padIds.padId).json.send(messageToTheOtherUsers);
-
+
//Run trough all sessions of this pad
async.forEach(socketio.sockets.clients(padIds.padId), function(roomClient, callback)
{
@@ -1059,9 +1169,9 @@ function handleClientReady(client, message)
//Jump over, if this session is the connection session
if(roomClient.id == client.id)
return callback();
-
-
- //Since sessioninfos might change while being enumerated, check if the
+
+
+ //Since sessioninfos might change while being enumerated, check if the
//sessionID is still assigned to a valid session
if(sessioninfos[roomClient.id] !== undefined)
author = sessioninfos[roomClient.id].author;
@@ -1077,7 +1187,7 @@ function handleClientReady(client, message)
callback(null, historicalAuthorData[author]);
else
authorManager.getAuthor(author, callback);
- },
+ },
function (authorInfo, callback)
{
//Send the new User a Notification about this other user
@@ -1106,7 +1216,7 @@ function handleClientReady(client, message)
}
/**
- * Handles a request for a rough changeset, the timeslider client needs it
+ * Handles a request for a rough changeset, the timeslider client needs it
*/
function handleChangesetRequest(client, message)
{
@@ -1136,7 +1246,7 @@ function handleChangesetRequest(client, message)
messageLogger.warn("Dropped message, changeset request has no requestID!");
return;
}
-
+
var granularity = message.data.granularity;
var start = message.data.start;
var end = start + (100 * granularity);
@@ -1154,7 +1264,7 @@ function handleChangesetRequest(client, message)
//build the requested rough changesets and send them back
getChangesetInfo(padIds.padId, start, end, granularity, function(err, changesetInfo)
{
- ERR(err);
+ if(err) return console.error('Error while handling a changeset request for '+padIds.padId, err, message.data);
var data = changesetInfo;
data.requestID = message.data.requestID;
@@ -1180,47 +1290,49 @@ function getChangesetInfo(padId, startNum, endNum, granularity, callback)
var composedChangesets = {};
var revisionDate = [];
var lines;
-
+ var head_revision = 0;
+
async.series([
//get the pad from the database
function(callback)
{
padManager.getPad(padId, function(err, _pad)
- {
+ {
if(ERR(err, callback)) return;
pad = _pad;
+ head_revision = pad.getHeadRevisionNumber();
callback();
});
},
function(callback)
- {
+ {
//calculate the last full endnum
var lastRev = pad.getHeadRevisionNumber();
if (endNum > lastRev+1) {
endNum = lastRev+1;
}
endNum = Math.floor(endNum / granularity)*granularity;
-
+
var compositesChangesetNeeded = [];
var revTimesNeeded = [];
-
+
//figure out which composite Changeset and revTimes we need, to load them in bulk
var compositeStart = startNum;
- while (compositeStart < endNum)
+ while (compositeStart < endNum)
{
var compositeEnd = compositeStart + granularity;
-
+
//add the composite Changeset we needed
compositesChangesetNeeded.push({start: compositeStart, end: compositeEnd});
-
+
//add the t1 time we need
revTimesNeeded.push(compositeStart == 0 ? 0 : compositeStart - 1);
//add the t2 time we need
revTimesNeeded.push(compositeEnd - 1);
-
+
compositeStart += granularity;
}
-
+
//get all needed db values parallel
async.parallel([
function(callback)
@@ -1257,58 +1369,57 @@ function getChangesetInfo(padId, startNum, endNum, granularity, callback)
if(ERR(err, callback)) return;
lines = _lines;
callback();
- });
+ });
}
], callback);
},
//doesn't know what happens here excatly :/
function(callback)
- {
+ {
var compositeStart = startNum;
-
- while (compositeStart < endNum)
+
+ while (compositeStart < endNum)
{
- if (compositeStart + granularity > endNum)
+ var compositeEnd = compositeStart + granularity;
+ if (compositeEnd > endNum || compositeEnd > head_revision+1)
{
break;
}
-
- var compositeEnd = compositeStart + granularity;
-
+
var forwards = composedChangesets[compositeStart + "/" + compositeEnd];
var backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool());
-
+
Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool());
Changeset.mutateTextLines(forwards, lines.textlines);
-
+
var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool);
var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool);
-
+
var t1, t2;
- if (compositeStart == 0)
+ if (compositeStart == 0)
{
t1 = revisionDate[0];
}
- else
+ else
{
t1 = revisionDate[compositeStart - 1];
}
-
+
t2 = revisionDate[compositeEnd - 1];
-
+
timeDeltas.push(t2 - t1);
forwardsChangesets.push(forwards2);
backwardsChangesets.push(backwards2);
-
+
compositeStart += granularity;
}
-
+
callback();
}
], function(err)
{
if(ERR(err, callback)) return;
-
+
callback(null, {forwardsChangesets: forwardsChangesets,
backwardsChangesets: backwardsChangesets,
apool: apool.toJsonable(),
@@ -1323,7 +1434,7 @@ function getChangesetInfo(padId, startNum, endNum, granularity, callback)
* Tries to rebuild the getPadLines function of the original Etherpad
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263
*/
-function getPadLines(padId, revNum, callback)
+function getPadLines(padId, revNum, callback)
{
var atext;
var result = {};
@@ -1334,7 +1445,7 @@ function getPadLines(padId, revNum, callback)
function(callback)
{
padManager.getPad(padId, function(err, _pad)
- {
+ {
if(ERR(err, callback)) return;
pad = _pad;
callback();
@@ -1378,7 +1489,7 @@ function getPadLines(padId, revNum, callback)
function composePadChangesets(padId, startNum, endNum, callback)
{
var pad;
- var changesets = [];
+ var changesets = {};
var changeset;
async.series([
@@ -1396,14 +1507,19 @@ function composePadChangesets(padId, startNum, endNum, callback)
function(callback)
{
var changesetsNeeded=[];
-
- //create a array for all changesets, we will
+
+ var headNum = pad.getHeadRevisionNumber();
+ if (endNum > headNum+1)
+ endNum = headNum+1;
+ if (startNum < 0)
+ startNum = 0;
+ //create a array for all changesets, we will
//replace the values with the changeset later
for(var r=startNum;r<endNum;r++)
{
changesetsNeeded.push(r);
}
-
+
//get all changesets
async.forEach(changesetsNeeded, function(revNum,callback)
{
@@ -1420,13 +1536,13 @@ function composePadChangesets(padId, startNum, endNum, callback)
{
changeset = changesets[startNum];
var pool = pad.apool();
-
+
for(var r=startNum+1;r<endNum;r++)
{
var cs = changesets[r];
changeset = Changeset.compose(changeset, cs, pool);
}
-
+
callback(null);
}
],
@@ -1461,7 +1577,10 @@ exports.padUsers = function (padID, callback) {
author.id = s.author;
result.push(author);
+ callback();
});
+ } else {
+ callback();
}
}, function(err) {
if(ERR(err, callback)) return;
@@ -1469,3 +1588,5 @@ exports.padUsers = function (padID, callback) {
callback(null, {padUsers: result});
});
}
+
+exports.sessioninfos = sessioninfos;
diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js
index 483bb1d1..b3e046d2 100644
--- a/src/node/handler/SocketIORouter.js
+++ b/src/node/handler/SocketIORouter.js
@@ -23,6 +23,8 @@ var ERR = require("async-stacktrace");
var log4js = require('log4js');
var messageLogger = log4js.getLogger("message");
var securityManager = require("../db/SecurityManager");
+var readOnlyManager = require("../db/ReadOnlyManager");
+var settings = require('../utils/Settings');
/**
* Saves all components
@@ -48,88 +50,68 @@ exports.addComponent = function(moduleName, module)
/**
* sets the socket.io and adds event functions for routing
*/
-exports.setSocketIO = function(_socket)
-{
+exports.setSocketIO = function(_socket) {
//save this socket internaly
socket = _socket;
socket.sockets.on('connection', function(client)
{
- client.set('remoteAddress', client.handshake.address.address);
+ if(settings.trustProxy && client.handshake.headers['x-forwarded-for'] !== undefined){
+ client.set('remoteAddress', client.handshake.headers['x-forwarded-for']);
+ }
+ else{
+ client.set('remoteAddress', client.handshake.address.address);
+ }
var clientAuthorized = false;
//wrap the original send function to log the messages
client._send = client.send;
- client.send = function(message)
- {
+ client.send = function(message) {
messageLogger.debug("to " + client.id + ": " + stringifyWithoutPassword(message));
client._send(message);
}
//tell all components about this connect
- for(var i in components)
- {
+ for(var i in components) {
components[i].handleConnect(client);
- }
-
- //try to handle the message of this client
- function handleMessage(message)
- {
- if(message.component && components[message.component])
- {
- //check if component is registered in the components array
- if(components[message.component])
- {
- messageLogger.debug("from " + client.id + ": " + stringifyWithoutPassword(message));
- components[message.component].handleMessage(client, message);
- }
- }
- else
- {
- messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message));
- }
- }
-
+ }
+
client.on('message', function(message)
{
- if(message.protocolVersion && message.protocolVersion != 2)
- {
+ if(message.protocolVersion && message.protocolVersion != 2) {
messageLogger.warn("Protocolversion header is not correct:" + stringifyWithoutPassword(message));
return;
}
//client is authorized, everything ok
- if(clientAuthorized)
- {
- handleMessage(message);
- }
- //try to authorize the client
- else
- {
- //this message has everything to try an authorization
- if(message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined)
- {
- securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, function(err, statusObject)
- {
+ if(clientAuthorized) {
+ handleMessage(client, message);
+ } else { //try to authorize the client
+ if(message.padId !== undefined && message.sessionID !== undefined && message.token !== undefined && message.password !== undefined) {
+ var checkAccessCallback = function(err, statusObject) {
ERR(err);
-
+
//access was granted, mark the client as authorized and handle the message
- if(statusObject.accessStatus == "grant")
- {
+ if(statusObject.accessStatus == "grant") {
clientAuthorized = true;
- handleMessage(message);
+ handleMessage(client, message);
}
//no access, send the client a message that tell him why
- else
- {
+ else {
messageLogger.warn("Authentication try failed:" + stringifyWithoutPassword(message));
client.json.send({accessStatus: statusObject.accessStatus});
}
- });
- }
- //drop message
- else
- {
+ };
+ if (message.padId.indexOf("r.") === 0) {
+ readOnlyManager.getPadId(message.padId, function(err, value) {
+ ERR(err);
+ securityManager.checkAccess (value, message.sessionID, message.token, message.password, checkAccessCallback);
+ });
+ } else {
+ //this message has everything to try an authorization
+ securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, checkAccessCallback);
+ }
+ } else { //drop message
messageLogger.warn("Dropped message cause of bad permissions:" + stringifyWithoutPassword(message));
}
}
@@ -146,6 +128,21 @@ exports.setSocketIO = function(_socket)
});
}
+//try to handle the message of this client
+function handleMessage(client, message)
+{
+
+ if(message.component && components[message.component]) {
+ //check if component is registered in the components array
+ if(components[message.component]) {
+ messageLogger.debug("from " + client.id + ": " + stringifyWithoutPassword(message));
+ components[message.component].handleMessage(client, message);
+ }
+ } else {
+ messageLogger.error("Can't route the message:" + stringifyWithoutPassword(message));
+ }
+}
+
//returns a stringified representation of a message, removes the password
//this ensures there are no passwords in the log
function stringifyWithoutPassword(message)
diff --git a/src/node/hooks/express.js b/src/node/hooks/express.js
index 61d9ae89..c6573c80 100644
--- a/src/node/hooks/express.js
+++ b/src/node/hooks/express.js
@@ -19,7 +19,7 @@ exports.createServer = function () {
var refPath = rootPath + "/.git/" + ref.substring(5, ref.indexOf("\n"));
version = fs.readFileSync(refPath, "utf-8");
version = version.substring(0, 7);
- console.log("Your Etherpad Lite git version is " + version);
+ console.log("Your Etherpad git version is " + version);
}
catch(e)
{
@@ -31,7 +31,7 @@ exports.createServer = function () {
exports.restartServer();
- console.log("You can access your Etherpad-Lite instance at http://" + settings.ip + ":" + settings.port + "/");
+ console.log("You can access your Etherpad instance at http://" + settings.ip + ":" + settings.port + "/");
if(!_.isEmpty(settings.users)){
console.log("The plugin admin page is at http://" + settings.ip + ":" + settings.port + "/admin/plugins");
}
@@ -80,6 +80,10 @@ exports.restartServer = function () {
next();
});
+ if(settings.trustProxy){
+ app.enable('trust proxy');
+ }
+
app.configure(function() {
hooks.callAll("expressConfigure", {"app": app});
});
diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js
index 7e221cf1..d8f19bba 100644
--- a/src/node/hooks/express/adminplugins.js
+++ b/src/node/hooks/express/adminplugins.js
@@ -27,49 +27,84 @@ exports.socketio = function (hook_name, args, cb) {
io.on('connection', function (socket) {
if (!socket.handshake.session.user || !socket.handshake.session.user.is_admin) return;
- socket.on("load", function (query) {
+ socket.on("getInstalled", function (query) {
// send currently installed plugins
- socket.emit("installed-results", {results: plugins.plugins});
- socket.emit("progress", {progress:1});
+ var installed = Object.keys(plugins.plugins).map(function(plugin) {
+ return plugins.plugins[plugin].package
+ })
+ socket.emit("results:installed", {installed: installed});
});
socket.on("checkUpdates", function() {
- socket.emit("progress", {progress:0, message:'Checking for plugin updates...'});
// Check plugins for updates
- installer.search({offset: 0, pattern: '', limit: 500}, /*useCache:*/true, function(data) { // hacky
- if (!data.results) return;
+ installer.getAvailablePlugins(/*maxCacheAge:*/60*10, function(er, results) {
+ if(er) {
+ console.warn(er);
+ socket.emit("results:updatable", {updatable: {}});
+ return;
+ }
var updatable = _(plugins.plugins).keys().filter(function(plugin) {
- if(!data.results[plugin]) return false;
- var latestVersion = data.results[plugin]['dist-tags'].latest
+ if(!results[plugin]) return false;
+ var latestVersion = results[plugin].version
var currentVersion = plugins.plugins[plugin].package.version
return semver.gt(latestVersion, currentVersion)
});
- socket.emit("updatable", {updatable: updatable});
- socket.emit("progress", {progress:1});
+ socket.emit("results:updatable", {updatable: updatable});
});
})
+
+ socket.on("getAvailable", function (query) {
+ installer.getAvailablePlugins(/*maxCacheAge:*/false, function (er, results) {
+ if(er) {
+ console.error(er)
+ results = {}
+ }
+ socket.emit("results:available", results);
+ });
+ });
socket.on("search", function (query) {
- socket.emit("progress", {progress:0, message:'Fetching results...'});
- installer.search(query, true, function (progress) {
- if (progress.results)
- socket.emit("search-result", progress);
- socket.emit("progress", progress);
+ installer.search(query.searchTerm, /*maxCacheAge:*/60*10, function (er, results) {
+ if(er) {
+ console.error(er)
+ results = {}
+ }
+ var res = Object.keys(results)
+ .map(function(pluginName) {
+ return results[pluginName]
+ })
+ .filter(function(plugin) {
+ return !plugins.plugins[plugin.name]
+ });
+ res = sortPluginList(res, query.sortBy, query.sortDir)
+ .slice(query.offset, query.offset+query.limit);
+ socket.emit("results:search", {results: res, query: query});
});
});
socket.on("install", function (plugin_name) {
- socket.emit("progress", {progress:0, message:'Downloading and installing ' + plugin_name + "..."});
- installer.install(plugin_name, function (progress) {
- socket.emit("progress", progress);
+ installer.install(plugin_name, function (er) {
+ if(er) console.warn(er)
+ socket.emit("finished:install", {plugin: plugin_name, error: er? er.message : null});
});
});
socket.on("uninstall", function (plugin_name) {
- socket.emit("progress", {progress:0, message:'Uninstalling ' + plugin_name + "..."});
- installer.uninstall(plugin_name, function (progress) {
- socket.emit("progress", progress);
+ installer.uninstall(plugin_name, function (er) {
+ if(er) console.warn(er)
+ socket.emit("finished:uninstall", {plugin: plugin_name, error: er? er.message : null});
});
});
});
}
+
+function sortPluginList(plugins, property, /*ASC?*/dir) {
+ return plugins.sort(function(a, b) {
+ if (a[property] < b[property])
+ return dir? -1 : 1;
+ if (a[property] > b[property])
+ return dir? 1 : -1;
+ // a must be equal to b
+ return 0;
+ })
+} \ No newline at end of file
diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.js
index 0971a877..db0fc81f 100644
--- a/src/node/hooks/express/apicalls.js
+++ b/src/node/hooks/express/apicalls.js
@@ -1,5 +1,6 @@
var log4js = require('log4js');
var apiLogger = log4js.getLogger("API");
+var clientLogger = log4js.getLogger("client");
var formidable = require('formidable');
var apiHandler = require('../../handler/APIHandler');
@@ -42,10 +43,10 @@ exports.expressCreateServer = function (hook_name, args, cb) {
});
});
- //The Etherpad client side sends information about how a disconnect happen
+ //The Etherpad client side sends information about how a disconnect happened
args.app.post('/ep/pad/connection-diagnostic-info', function(req, res) {
new formidable.IncomingForm().parse(req, function(err, fields, files) {
- console.log("DIAGNOSTIC-INFO: " + fields.diagnosticInfo);
+ clientLogger.info("DIAGNOSTIC-INFO: " + fields.diagnosticInfo);
res.end("OK");
});
});
@@ -53,7 +54,12 @@ exports.expressCreateServer = function (hook_name, args, cb) {
//The Etherpad client side sends information about client side javscript errors
args.app.post('/jserror', function(req, res) {
new formidable.IncomingForm().parse(req, function(err, fields, files) {
- console.error("CLIENT SIDE JAVASCRIPT ERROR: " + fields.errorInfo);
+ try {
+ var data = JSON.parse(fields.errorInfo)
+ }catch(e){
+ return res.end()
+ }
+ clientLogger.warn(data.msg+' --', data);
res.end("OK");
});
});
diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.js
index 3c595683..087dd50e 100644
--- a/src/node/hooks/express/errorhandling.js
+++ b/src/node/hooks/express/errorhandling.js
@@ -1,5 +1,6 @@
var os = require("os");
var db = require('../../db/DB');
+var stats = require('ep_etherpad-lite/node/stats')
exports.onShutdown = false;
@@ -28,6 +29,7 @@ exports.gracefulShutdown = function(err) {
}, 3000);
}
+process.on('uncaughtException', exports.gracefulShutdown);
exports.expressCreateServer = function (hook_name, args, cb) {
exports.app = args.app;
@@ -39,6 +41,7 @@ exports.expressCreateServer = function (hook_name, args, cb) {
// allowing you to respond however you like
res.send(500, { error: 'Sorry, something bad happened!' });
console.error(err.stack? err.stack : err.toString());
+ stats.meter('http500').mark()
})
//connect graceful shutdown with sigint and uncaughtexception
@@ -47,6 +50,4 @@ exports.expressCreateServer = function (hook_name, args, cb) {
//https://github.com/joyent/node/issues/1553
process.on('SIGINT', exports.gracefulShutdown);
}
-
- process.on('uncaughtException', exports.gracefulShutdown);
-}
+} \ No newline at end of file
diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js
index 9754ffa6..f5a3e5a1 100644
--- a/src/node/hooks/express/importexport.js
+++ b/src/node/hooks/express/importexport.js
@@ -15,7 +15,7 @@ exports.expressCreateServer = function (hook_name, args, cb) {
//if abiword is disabled, and this is a format we only support with abiword, output a message
if (settings.abiword == null &&
["odt", "pdf", "doc"].indexOf(req.params.type) !== -1) {
- res.send("Abiword is not enabled at this Etherpad Lite instance. Set the path to Abiword in settings.json to enable this feature");
+ res.send("Abiword is not enabled at this Etherpad instance. Set the path to Abiword in settings.json to enable this feature");
return;
}
diff --git a/src/node/hooks/express/padreadonly.js b/src/node/hooks/express/padreadonly.js
index af5cbed3..9a0a52bf 100644
--- a/src/node/hooks/express/padreadonly.js
+++ b/src/node/hooks/express/padreadonly.js
@@ -16,50 +16,50 @@ exports.expressCreateServer = function (hook_name, args, cb) {
//translate the read only pad to a padId
function(callback)
{
- readOnlyManager.getPadId(req.params.id, function(err, _padId)
- {
- if(ERR(err, callback)) return;
+ readOnlyManager.getPadId(req.params.id, function(err, _padId)
+ {
+ if(ERR(err, callback)) return;
- padId = _padId;
+ padId = _padId;
- //we need that to tell hasPadAcess about the pad
- req.params.pad = padId;
+ //we need that to tell hasPadAcess about the pad
+ req.params.pad = padId;
- callback();
- });
+ callback();
+ });
},
//render the html document
function(callback)
{
- //return if the there is no padId
- if(padId == null)
- {
- callback("notfound");
- return;
- }
+ //return if the there is no padId
+ if(padId == null)
+ {
+ callback("notfound");
+ return;
+ }
- hasPadAccess(req, res, function()
- {
- //render the html document
- exporthtml.getPadHTMLDocument(padId, null, false, function(err, _html)
- {
- if(ERR(err, callback)) return;
- html = _html;
- callback();
- });
- });
+ hasPadAccess(req, res, function()
+ {
+ //render the html document
+ exporthtml.getPadHTMLDocument(padId, null, false, function(err, _html)
+ {
+ if(ERR(err, callback)) return;
+ html = _html;
+ callback();
+ });
+ });
}
], function(err)
{
//throw any unexpected error
if(err && err != "notfound")
- ERR(err);
+ ERR(err);
if(err == "notfound")
- res.send(404, '404 - Not Found');
+ res.send(404, '404 - Not Found');
else
- res.send(html);
+ res.send(html);
});
});
-} \ No newline at end of file
+}
diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js
index 29782b69..2aadccdc 100644
--- a/src/node/hooks/express/padurlsanitize.js
+++ b/src/node/hooks/express/padurlsanitize.js
@@ -12,20 +12,20 @@ exports.expressCreateServer = function (hook_name, args, cb) {
else
{
padManager.sanitizePadId(padId, function(sanitizedPadId) {
- //the pad id was sanitized, so we redirect to the sanitized version
- if(sanitizedPadId != padId)
- {
+ //the pad id was sanitized, so we redirect to the sanitized version
+ if(sanitizedPadId != padId)
+ {
var real_url = sanitizedPadId;
var query = url.parse(req.url).query;
if ( query ) real_url += '?' + query;
- res.header('Location', real_url);
- res.send(302, 'You should be redirected to <a href="' + real_url + '">' + real_url + '</a>');
- }
- //the pad id was fine, so just render it
- else
- {
- next();
- }
+ res.header('Location', real_url);
+ res.send(302, 'You should be redirected to <a href="' + real_url + '">' + real_url + '</a>');
+ }
+ //the pad id was fine, so just render it
+ else
+ {
+ next();
+ }
});
}
});
diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js
index 44002461..063328fb 100644
--- a/src/node/hooks/express/specialpages.js
+++ b/src/node/hooks/express/specialpages.js
@@ -1,7 +1,13 @@
var path = require('path');
var eejs = require('ep_etherpad-lite/node/eejs');
+var toolbar = require("ep_etherpad-lite/node/utils/toolbar");
+var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
exports.expressCreateServer = function (hook_name, args, cb) {
+ // expose current stats
+ args.app.get('/stats', function(req, res) {
+ res.json(require('ep_etherpad-lite/node/stats').toJSON())
+ })
//serve index.html under /
args.app.get('/', function(req, res)
@@ -26,14 +32,28 @@ exports.expressCreateServer = function (hook_name, args, cb) {
//serve pad.html under /p
args.app.get('/p/:pad', function(req, res, next)
- {
- res.send(eejs.require("ep_etherpad-lite/templates/pad.html", {req: req}));
+ {
+ hooks.callAll("padInitToolbar", {
+ toolbar: toolbar
+ });
+
+ res.send(eejs.require("ep_etherpad-lite/templates/pad.html", {
+ req: req,
+ toolbar: toolbar
+ }));
});
//serve timeslider.html under /p/$padname/timeslider
args.app.get('/p/:pad/timeslider', function(req, res, next)
{
- res.send(eejs.require("ep_etherpad-lite/templates/timeslider.html", {req: req}));
+ hooks.callAll("padInitToolbar", {
+ toolbar: toolbar
+ });
+
+ res.send(eejs.require("ep_etherpad-lite/templates/timeslider.html", {
+ req: req,
+ toolbar: toolbar
+ }));
});
//serve favicon.ico from all path levels except as a pad name
@@ -45,11 +65,11 @@ exports.expressCreateServer = function (hook_name, args, cb) {
//there is no custom favicon, send the default favicon
if(err)
{
- filePath = path.normalize(__dirname + "/../../../static/favicon.ico");
- res.sendfile(filePath);
+ filePath = path.normalize(__dirname + "/../../../static/favicon.ico");
+ res.sendfile(filePath);
}
});
});
-} \ No newline at end of file
+}
diff --git a/src/node/hooks/express/swagger.js b/src/node/hooks/express/swagger.js
index f4fc5cff..e8daa61c 100644
--- a/src/node/hooks/express/swagger.js
+++ b/src/node/hooks/express/swagger.js
@@ -86,7 +86,7 @@ var API = {
},
"createIfNotExistsFor" : {
"func": "createGroupIfNotExistsFor",
- "description": "this functions helps you to map your application group ids to etherpad lite group ids",
+ "description": "this functions helps you to map your application group ids to Etherpad group ids",
"response": {"groupID":{"type":"string"}}
},
"delete" : {
@@ -124,7 +124,7 @@ var API = {
},
"createIfNotExistsFor": {
"func": "createAuthorIfNotExistsFor",
- "description": "this functions helps you to map your application author ids to etherpad lite author ids",
+ "description": "this functions helps you to map your application author ids to Etherpad author ids",
"response": {"authorID":{"type":"string"}}
},
"listPads": {
@@ -354,7 +354,6 @@ exports.expressCreateServer = function (hook_name, args, cb) {
// Let's put this under /rest for now
var subpath = express();
- args.app.use(express.bodyParser());
args.app.use(basePath, subpath);
swagger.setAppHandler(subpath);
diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js
index c39f91da..6998853f 100644
--- a/src/node/hooks/express/webaccess.js
+++ b/src/node/hooks/express/webaccess.js
@@ -5,6 +5,7 @@ var settings = require('../../utils/Settings');
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
var ueberStore = require('../../db/SessionStore');
+var stats = require('ep_etherpad-lite/node/stats')
//checks for basic http auth
exports.basicAuth = function (req, res, next) {
@@ -32,15 +33,15 @@ exports.basicAuth = function (req, res, next) {
// If auth headers are present use them to authenticate...
if (req.headers.authorization && req.headers.authorization.search('Basic ') === 0) {
var userpass = new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString().split(":")
- var username = userpass[0];
- var password = userpass[1];
+ var username = userpass.shift();
+ var password = userpass.join(':');
if (settings.users[username] != undefined && settings.users[username].password == password) {
settings.users[username].username = username;
req.session.user = settings.users[username];
return cb(true);
}
- return hooks.aCallFirst("authenticate", {req: req, res:res, next:next, username: username, password: password}, hookResultMangle(cb));
+ return hooks.aCallFirst("authenticate", {req: req, res:res, next:next, username: username, password: password}, hookResultMangle(cb));
}
hooks.aCallFirst("authenticate", {req: req, res:res, next:next}, hookResultMangle(cb));
}
@@ -91,10 +92,21 @@ exports.basicAuth = function (req, res, next) {
exports.secret = null;
exports.expressConfigure = function (hook_name, args, cb) {
+ // Measure response time
+ args.app.use(function(req, res, next) {
+ var stopWatch = stats.timer('httpRequests').start();
+ var sendFn = res.send
+ res.send = function() {
+ stopWatch.end()
+ sendFn.apply(res, arguments)
+ }
+ next()
+ })
+
// If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158.
// Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway.
if (!(settings.loglevel === "WARN" || settings.loglevel == "ERROR"))
- args.app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'}));
+ args.app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.DEBUG, format: ':status, :method :url'}));
/* Do not let express create the session, so that we can retain a
* reference to it for socket.io to use. Also, set the key (cookie
diff --git a/src/node/server.js b/src/node/server.js
index db75d7e3..605ce847 100755
--- a/src/node/server.js
+++ b/src/node/server.js
@@ -23,10 +23,15 @@
var log4js = require('log4js')
, async = require('async')
+ , stats = require('./stats')
;
log4js.replaceConsole();
+stats.gauge('memoryUsage', function() {
+ return process.memoryUsage().rss
+})
+
var settings
, db
, plugins
@@ -48,7 +53,6 @@ async.waterfall([
plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
hooks.plugins = plugins;
-
callback();
},
diff --git a/src/node/stats.js b/src/node/stats.js
new file mode 100644
index 00000000..24efaf4a
--- /dev/null
+++ b/src/node/stats.js
@@ -0,0 +1,3 @@
+var measured = require('measured')
+
+module.exports = measured.createCollection(); \ No newline at end of file
diff --git a/src/node/utils/Abiword.js b/src/node/utils/Abiword.js
index 27138e64..5f12bd97 100644
--- a/src/node/utils/Abiword.js
+++ b/src/node/utils/Abiword.js
@@ -63,7 +63,7 @@ if(os.type().indexOf("Windows") > -1)
callback();
});
- }
+ };
exports.convertFile = function(srcFile, destFile, type, callback)
{
@@ -100,7 +100,7 @@ else
{
//add data to buffer
stdoutBuffer+=data.toString();
-
+
//we're searching for the prompt, cause this means everything we need is in the buffer
if(stdoutBuffer.search("AbiWord:>") != -1)
{
@@ -121,27 +121,29 @@ else
firstPrompt = false;
}
});
- }
+ };
spawnAbiword();
doConvertTask = function(task, callback)
{
abiword.stdin.write("convert " + task.srcFile + " " + task.destFile + " " + task.type + "\n");
-
//create a callback that calls the task callback and the caller callback
stdoutCallback = function (err)
{
callback();
console.log("queue continue");
- task.callback(err);
+ try{
+ task.callback(err);
+ }catch(e){
+ console.error("Abiword File failed to convert", e);
+ }
};
- }
+ };
//Queue with the converts we have to do
var queue = async.queue(doConvertTask, 1);
-
exports.convertFile = function(srcFile, destFile, type, callback)
- {
+ {
queue.push({"srcFile": srcFile, "destFile": destFile, "type": type, "callback": callback});
};
}
diff --git a/src/node/utils/ExportDokuWiki.js b/src/node/utils/ExportDokuWiki.js
index d2f71236..f5d2d177 100644
--- a/src/node/utils/ExportDokuWiki.js
+++ b/src/node/utils/ExportDokuWiki.js
@@ -316,7 +316,7 @@ exports.getPadDokuWikiDocument = function (padId, revNum, callback)
getPadDokuWiki(pad, revNum, callback);
});
-}
+};
function _escapeDokuWiki(s)
{
diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.js
index a939a8b6..136896f0 100644
--- a/src/node/utils/ExportHelper.js
+++ b/src/node/utils/ExportHelper.js
@@ -45,7 +45,7 @@ exports.getPadPlainText = function(pad, revNum){
}
return pieces.join('');
-}
+};
exports._analyzeLine = function(text, aline, apool){
@@ -77,11 +77,11 @@ exports._analyzeLine = function(text, aline, apool){
line.aline = aline;
}
return line;
-}
+};
exports._encodeWhitespace = function(s){
return s.replace(/[^\x21-\x7E\s\t\n\r]/g, function(c){
- return "&#" +c.charCodeAt(0) + ";"
+ return "&#" +c.charCodeAt(0) + ";";
});
-}
+};
diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js
index 585694d4..01920da7 100644
--- a/src/node/utils/ExportHtml.js
+++ b/src/node/utils/ExportHtml.js
@@ -21,7 +21,6 @@ var padManager = require("../db/PadManager");
var ERR = require("async-stacktrace");
var Security = require('ep_etherpad-lite/static/js/security');
var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
-var getPadPlainText = require('./ExportHelper').getPadPlainText
var _analyzeLine = require('./ExportHelper')._analyzeLine;
var _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
@@ -79,6 +78,10 @@ function getHTMLFromAtext(pad, atext, authorColors)
var tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
+ // holds a map of used styling attributes (*1, *2, etc) in the apool
+ // and maps them to an index in props
+ // *3:2 -> the attribute *3 means strong
+ // *2:5 -> the attribute *2 means s(trikethrough)
var anumMap = {};
var css = "";
@@ -88,24 +91,24 @@ function getHTMLFromAtext(pad, atext, authorColors)
if(authorColors){
css+="<style>\n";
-
+
for (var a in apool.numToAttrib) {
var attr = apool.numToAttrib[a];
-
+
//skip non author attributes
if(attr[0] === "author" && attr[1] !== ""){
//add to props array
var propName = "author" + stripDotFromAuthorID(attr[1]);
var newLength = props.push(propName);
anumMap[a] = newLength -1;
-
+
css+="." + propName + " {background-color: " + authorColors[attr[1]]+ "}\n";
} else if(attr[0] === "removed") {
var propName = "removed";
-
+
var newLength = props.push(propName);
anumMap[a] = newLength -1;
-
+
css+=".removed {text-decoration: line-through; " +
"-ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=80)'; "+
"filter: alpha(opacity=80); "+
@@ -113,10 +116,12 @@ function getHTMLFromAtext(pad, atext, authorColors)
"}\n";
}
}
-
+
css+="</style>";
}
+ // iterates over all props(h1,h2,strong,...), checks if it is used in
+ // this pad, and if yes puts its attrib id->props value into anumMap
props.forEach(function (propName, i)
{
var propTrueNum = apool.putAttrib([propName, true], true);
@@ -128,11 +133,6 @@ function getHTMLFromAtext(pad, atext, authorColors)
function getLineHTML(text, attribs)
{
- var propVals = [false, false, false];
- var ENTER = 1;
- var STAY = 2;
- var LEAVE = 0;
-
// Use order of tags (b/i/u) as order of nesting, for simplicity
// and decent nesting. For example,
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
@@ -145,17 +145,17 @@ function getHTMLFromAtext(pad, atext, authorColors)
function getSpanClassFor(i){
//return if author colors are disabled
if (!authorColors) return false;
-
+
var property = props[i];
-
+
if(property.substr(0,6) === "author"){
return stripDotFromAuthorID(property);
}
-
+
if(property === "removed"){
return "removed";
}
-
+
return false;
}
@@ -163,7 +163,7 @@ function getHTMLFromAtext(pad, atext, authorColors)
{
openTags.unshift(i);
var spanClass = getSpanClassFor(i);
-
+
if(spanClass){
assem.append('<span class="');
assem.append(spanClass);
@@ -175,11 +175,12 @@ function getHTMLFromAtext(pad, atext, authorColors)
}
}
+ // this closes an open tag and removes its reference from openTags
function emitCloseTag(i)
{
openTags.shift();
var spanClass = getSpanClassFor(i);
-
+
if(spanClass){
assem.append('</span>');
} else {
@@ -188,22 +189,6 @@ function getHTMLFromAtext(pad, atext, authorColors)
assem.append('>');
}
}
-
- function orderdCloseTags(tags2close)
- {
- for(var i=0;i<openTags.length;i++)
- {
- for(var j=0;j<tags2close.length;j++)
- {
- if(tags2close[j] == openTags[i])
- {
- emitCloseTag(tags2close[j]);
- i--;
- break;
- }
- }
- }
- }
var urls = _findURLs(text);
@@ -219,118 +204,71 @@ function getHTMLFromAtext(pad, atext, authorColors)
var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars));
idx += numChars;
+ // this iterates over every op string and decides which tags to open or to close
+ // based on the attribs used
while (iter.hasNext())
{
var o = iter.next();
- var propChanged = false;
+ var usedAttribs = [];
+
+ // mark all attribs as used
Changeset.eachAttribNumber(o.attribs, function (a)
{
if (a in anumMap)
{
- var i = anumMap[a]; // i = 0 => bold, etc.
- if (!propVals[i])
- {
- propVals[i] = ENTER;
- propChanged = true;
- }
- else
- {
- propVals[i] = STAY;
- }
+ usedAttribs.push(anumMap[a]); // i = 0 => bold, etc.
}
});
- for (var i = 0; i < propVals.length; i++)
+ var outermostTag = -1;
+ // find the outer most open tag that is no longer used
+ for (var i = openTags.length - 1; i >= 0; i--)
{
- if (propVals[i] === true)
- {
- propVals[i] = LEAVE;
- propChanged = true;
- }
- else if (propVals[i] === STAY)
+ if (usedAttribs.indexOf(openTags[i]) === -1)
{
- propVals[i] = true; // set it back
+ outermostTag = i;
+ break;
}
}
- // now each member of propVal is in {false,LEAVE,ENTER,true}
- // according to what happens at start of span
- if (propChanged)
+
+ // close all tags upto the outer most
+ if (outermostTag != -1)
{
- // leaving bold (e.g.) also leaves italics, etc.
- var left = false;
- for (var i = 0; i < propVals.length; i++)
+ while ( outermostTag >= 0 )
{
- var v = propVals[i];
- if (!left)
- {
- if (v === LEAVE)
- {
- left = true;
- }
- }
- else
- {
- if (v === true)
- {
- propVals[i] = STAY; // tag will be closed and re-opened
- }
- }
+ emitCloseTag(openTags[0]);
+ outermostTag--;
}
+ }
- var tags2close = [];
-
- for (var i = propVals.length - 1; i >= 0; i--)
- {
- if (propVals[i] === LEAVE)
- {
- //emitCloseTag(i);
- tags2close.push(i);
- propVals[i] = false;
- }
- else if (propVals[i] === STAY)
- {
- //emitCloseTag(i);
- tags2close.push(i);
- }
- }
-
- orderdCloseTags(tags2close);
-
- for (var i = 0; i < propVals.length; i++)
+ // open all tags that are used but not open
+ for (i=0; i < usedAttribs.length; i++)
+ {
+ if (openTags.indexOf(usedAttribs[i]) === -1)
{
- if (propVals[i] === ENTER || propVals[i] === STAY)
- {
- emitOpenTag(i);
- propVals[i] = true;
- }
+ emitOpenTag(usedAttribs[i])
}
- // propVals is now all {true,false} again
- } // end if (propChanged)
+ }
+
var chars = o.chars;
if (o.lines)
{
chars--; // exclude newline at end of line, if present
}
-
+
var s = taker.take(chars);
-
+
//removes the characters with the code 12. Don't know where they come
//from but they break the abiword parser and are completly useless
s = s.replace(String.fromCharCode(12), "");
-
+
assem.append(_encodeWhitespace(Security.escapeHTML(s)));
} // end iteration over spans in line
-
- var tags2close = [];
- for (var i = propVals.length - 1; i >= 0; i--)
+
+ // close all the tags that are open after the last op
+ while (openTags.length > 0)
{
- if (propVals[i])
- {
- tags2close.push(i);
- propVals[i] = false;
- }
+ emitCloseTag(openTags[0])
}
-
- orderdCloseTags(tags2close);
} // end processNextChars
if (urls)
{
@@ -363,7 +301,7 @@ function getHTMLFromAtext(pad, atext, authorColors)
{
var line = _analyzeLine(textLines[i], attribLines[i], apool);
var lineContent = getLineHTML(line.text, line.aline);
-
+
if (line.listLevel)//If we are inside a list
{
// do list stuff
@@ -447,7 +385,7 @@ function getHTMLFromAtext(pad, atext, authorColors)
pieces.push('</li></ul>');
}
lists.length--;
- }
+ }
var lineContentFromHook = hooks.callAllStr("getLineHTMLForExport",
{
line: line,
@@ -455,14 +393,14 @@ function getHTMLFromAtext(pad, atext, authorColors)
attribLine: attribLines[i],
text: textLines[i]
}, " ", " ", "");
- if (lineContentFromHook)
- {
- pieces.push(lineContentFromHook, '');
- }
- else
- {
- pieces.push(lineContent, '<br>');
- }
+ if (lineContentFromHook)
+ {
+ pieces.push(lineContentFromHook, '');
+ }
+ else
+ {
+ pieces.push(lineContent, '<br>');
+ }
}
}
@@ -490,7 +428,7 @@ exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback)
var head =
(noDocType ? '' : '<!doctype html>\n') +
'<html lang="en">\n' + (noDocType ? '' : '<head>\n' +
- '<title>' + Security.escapeHTML(padId) + '</title>\n' +
+ '<title>' + Security.escapeHTML(padId) + '</title>\n' +
'<meta charset="utf-8">\n' +
'<style> * { font-family: arial, sans-serif;\n' +
'font-size: 13px;\n' +
@@ -515,7 +453,7 @@ exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback)
callback(null, head + html + foot);
});
});
-}
+};
// copied from ACE
@@ -573,8 +511,8 @@ function _processSpaces(s){
}
}
// beginning of line is nbsp
- for (var i = 0; i < parts.length; i++){
- var p = parts[i];
+ for (i = 0; i < parts.length; i++){
+ p = parts[i];
if (p == " "){
parts[i] = '&nbsp;';
break;
@@ -586,8 +524,8 @@ function _processSpaces(s){
}
else
{
- for (var i = 0; i < parts.length; i++){
- var p = parts[i];
+ for (i = 0; i < parts.length; i++){
+ p = parts[i];
if (p == " "){
parts[i] = '&nbsp;';
}
@@ -595,4 +533,3 @@ function _processSpaces(s){
}
return parts.join('');
}
-
diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.js
index c57424f1..f0b62743 100644
--- a/src/node/utils/ExportTxt.js
+++ b/src/node/utils/ExportTxt.js
@@ -289,5 +289,4 @@ exports.getPadTXTDocument = function (padId, revNum, noDocType, callback)
callback(null, html);
});
});
-}
-
+};
diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js
index 7c638fb8..abba2ac1 100644
--- a/src/node/utils/ImportHtml.js
+++ b/src/node/utils/ImportHtml.js
@@ -25,21 +25,29 @@ function setPadHTML(pad, html, callback)
{
var apiLogger = log4js.getLogger("ImportHtml");
- // Clean the pad. This makes the rest of the code easier
- // by several orders of magnitude.
- pad.setText("");
- var padText = pad.text();
-
// Parse the incoming HTML with jsdom
- var doc = jsdom(html.replace(/>\n+</g, '><'));
+ try{
+ var doc = jsdom(html.replace(/>\n+</g, '><'));
+ }catch(e){
+ apiLogger.warn("Error importing, possibly caused by malformed HTML");
+ var doc = jsdom("<html><body><div>Error during import, possibly malformed HTML</div></body></html>");
+ }
+
apiLogger.debug('html:');
apiLogger.debug(html);
// Convert a dom tree into a list of lines and attribute liens
// using the content collector object
var cc = contentcollector.makeContentCollector(true, null, pad.pool);
- cc.collectContent(doc.childNodes[0]);
+ try{ // we use a try here because if the HTML is bad it will blow up
+ cc.collectContent(doc.childNodes[0]);
+ }catch(e){
+ apiLogger.warn("HTML was not properly formed", e);
+ return; // We don't process the HTML because it was bad..
+ }
+
var result = cc.finish();
+
apiLogger.debug('Lines:');
var i;
for (i = 0; i < result.lines.length; i += 1)
@@ -84,6 +92,7 @@ function setPadHTML(pad, html, callback)
// the changeset is ready!
var theChangeset = builder.toString();
apiLogger.debug('The changeset: ' + theChangeset);
+ pad.setText("");
pad.appendRevision(theChangeset);
}
diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js
index 5fc8accb..58d08b30 100644
--- a/src/node/utils/Minify.js
+++ b/src/node/utils/Minify.js
@@ -125,11 +125,11 @@ function requestURIs(locations, method, headers, callback) {
}
function completed() {
- var statuss = responses.map(function (x) {return x[0]});
- var headerss = responses.map(function (x) {return x[1]});
- var contentss = responses.map(function (x) {return x[2]});
+ var statuss = responses.map(function (x) {return x[0];});
+ var headerss = responses.map(function (x) {return x[1];});
+ var contentss = responses.map(function (x) {return x[2];});
callback(statuss, headerss, contentss);
- };
+ }
}
/**
@@ -263,7 +263,7 @@ function getAceFile(callback) {
var filename = item.match(/"([^"]*)"/)[1];
var request = require('request');
- var baseURI = 'http://localhost:' + settings.port
+ var baseURI = 'http://localhost:' + settings.port;
var resourceURI = baseURI + path.normalize(path.join('/static/', filename));
resourceURI = resourceURI.replace(/\\/g, '/'); // Windows (safe generally?)
diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js
index 45f81aa5..4c6b7ea4 100644
--- a/src/node/utils/Settings.js
+++ b/src/node/utils/Settings.js
@@ -1,5 +1,5 @@
/**
- * The Settings Modul reads the settings out of settings.json and provides
+ * The Settings Modul reads the settings out of settings.json and provides
* this information to the other modules
*/
@@ -24,9 +24,9 @@ var os = require("os");
var path = require('path');
var argv = require('./Cli').argv;
var npm = require("npm/lib/npm.js");
-var vm = require('vm');
+var jsonminify = require("jsonminify");
var log4js = require("log4js");
-var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
+var randomString = require("./randomstring");
/* Root path of the installation */
@@ -35,7 +35,7 @@ exports.root = path.normalize(path.join(npm.dir, ".."));
/**
* The app title, visible e.g. in the browser window
*/
-exports.title = "Etherpad Lite";
+exports.title = "Etherpad";
/**
* The app favicon fully specified url, visible e.g. in the browser window
@@ -48,7 +48,7 @@ exports.faviconTimeslider = "../../" + exports.favicon;
* The IP ep-lite should listen to
*/
exports.ip = "0.0.0.0";
-
+
/**
* The Port ep-lite should listen to
*/
@@ -77,7 +77,27 @@ exports.dbSettings = { "filename" : path.join(exports.root, "dirty.db") };
/**
* The default Text of a new pad
*/
-exports.defaultPadText = "Welcome to Etherpad Lite!\n\nThis 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!\n\nEtherpad Lite on Github: http:\/\/j.mp/ep-lite\n";
+exports.defaultPadText = "Welcome to Etherpad!\n\nThis 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!\n\nEtherpad on Github: http:\/\/j.mp/ep-lite\n";
+
+/**
+ * The toolbar buttons and order.
+ */
+exports.toolbar = {
+ left: [
+ ["bold", "italic", "underline", "strikethrough"],
+ ["orderedlist", "unorderedlist", "indent", "outdent"],
+ ["undo", "redo"],
+ ["clearauthorship"]
+ ],
+ right: [
+ ["importexport", "timeslider", "savedrevision"],
+ ["settings", "embed"],
+ ["showusers"]
+ ],
+ timeslider: [
+ ["timeslider_export", "timeslider_returnToPad"]
+ ]
+}
/**
* A flag that requires any user to have a valid session (via the api) before accessing a pad
@@ -109,6 +129,11 @@ exports.abiword = null;
*/
exports.loglevel = "INFO";
+/**
+ * Disable IP logging
+ */
+exports.disableIPlogging = false;
+
/*
* log4js appender configuration
*/
@@ -119,6 +144,11 @@ exports.logconfig = { appenders: [{ type: "console" }]};
*/
exports.sessionKey = false;
+/*
+* Trust Proxy, whether or not trust the x-forwarded-for header.
+*/
+exports.trustProxy = false;
+
/* This setting is used if you need authentication and/or
* authorization. Note: /admin always requires authentication, and
* either authorization by a module, or a user with is_admin set */
@@ -137,12 +167,12 @@ exports.abiwordAvailable = function()
{
return "no";
}
-}
+};
exports.reloadSettings = function reloadSettings() {
// Discover where the settings file lives
var settingsFilename = argv.settings || "settings.json";
- settingsFilename = path.resolve(path.join(root, settingsFilename));
+ settingsFilename = path.resolve(path.join(exports.root, settingsFilename));
var settingsStr;
try{
@@ -156,8 +186,8 @@ exports.reloadSettings = function reloadSettings() {
var settings;
try {
if(settingsStr) {
- settings = vm.runInContext('exports = '+settingsStr, vm.createContext(), "settings.json");
- settings = JSON.parse(JSON.stringify(settings)) // fix objects having constructors of other vm.context
+ settingsStr = jsonminify(settingsStr).replace(",]","]").replace(",}","}");
+ settings = JSON.parse(settingsStr);
}
}catch(e){
console.error('There was an error processing your settings.json file: '+e.message);
@@ -185,7 +215,7 @@ exports.reloadSettings = function reloadSettings() {
console.warn("Unknown Setting: '" + i + "'. This setting doesn't exist or it was removed");
}
}
-
+
log4js.configure(exports.logconfig);//Configure the logging appenders
log4js.setGlobalLogLevel(exports.loglevel);//set loglevel
log4js.replaceConsole();
@@ -196,9 +226,9 @@ exports.reloadSettings = function reloadSettings() {
}
if(exports.dbType === "dirty"){
- console.warn("DirtyDB is used. This is fine for testing but not recommended for production.")
+ console.warn("DirtyDB is used. This is fine for testing but not recommended for production.");
}
-}
+};
// initially load settings
exports.reloadSettings();
diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js
index c6b23713..d30dc398 100644
--- a/src/node/utils/caching_middleware.js
+++ b/src/node/utils/caching_middleware.js
@@ -23,7 +23,7 @@ var util = require('util');
var settings = require('./Settings');
var semver = require('semver');
-var existsSync = (semver.satisfies(process.version, '>=0.8.0')) ? fs.existsSync : path.existsSync
+var existsSync = (semver.satisfies(process.version, '>=0.8.0')) ? fs.existsSync : path.existsSync;
var CACHE_DIR = path.normalize(path.join(settings.root, 'var/'));
CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
@@ -133,7 +133,7 @@ CachingMiddleware.prototype = new function () {
old_res.write = res.write;
old_res.end = res.end;
res.write = function(data, encoding) {};
- res.end = function(data, encoding) { respond() };
+ res.end = function(data, encoding) { respond(); };
} else {
res.writeHead(status, headers);
}
@@ -168,7 +168,7 @@ CachingMiddleware.prototype = new function () {
} else if (req.method == 'GET') {
var readStream = fs.createReadStream(pathStr);
res.writeHead(statusCode, headers);
- util.pump(readStream, res);
+ readStream.pipe(res);
} else {
res.writeHead(statusCode, headers);
res.end();
diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js
index 1b3cf58f..88fa5cba 100644
--- a/src/node/utils/padDiff.js
+++ b/src/node/utils/padDiff.js
@@ -68,7 +68,7 @@ PadDiff.prototype._isClearAuthorship = function(changeset){
return false;
return true;
-}
+};
PadDiff.prototype._createClearAuthorship = function(rev, callback){
var self = this;
@@ -84,7 +84,7 @@ PadDiff.prototype._createClearAuthorship = function(rev, callback){
callback(null, changeset);
});
-}
+};
PadDiff.prototype._createClearStartAtext = function(rev, callback){
var self = this;
@@ -107,7 +107,7 @@ PadDiff.prototype._createClearStartAtext = function(rev, callback){
callback(null, newAText);
});
});
-}
+};
PadDiff.prototype._getChangesetsInBulk = function(startRev, count, callback) {
var self = this;
@@ -124,7 +124,7 @@ PadDiff.prototype._getChangesetsInBulk = function(startRev, count, callback) {
async.forEach(revisions, function(rev, callback){
self._pad.getRevision(rev, function(err, revision){
if(err){
- return callback(err)
+ return callback(err);
}
var arrayNum = rev-startRev;
@@ -137,7 +137,7 @@ PadDiff.prototype._getChangesetsInBulk = function(startRev, count, callback) {
}, function(err){
callback(err, changesets, authors);
});
-}
+};
PadDiff.prototype._addAuthors = function(authors) {
var self = this;
@@ -147,7 +147,7 @@ PadDiff.prototype._addAuthors = function(authors) {
self._authors.push(author);
}
});
-}
+};
PadDiff.prototype._createDiffAtext = function(callback) {
var self = this;
@@ -219,7 +219,7 @@ PadDiff.prototype._createDiffAtext = function(callback) {
}
);
});
-}
+};
PadDiff.prototype.getHtml = function(callback){
//cache the html
@@ -279,7 +279,7 @@ PadDiff.prototype.getAuthors = function(callback){
} else {
callback(null, self._authors);
}
-}
+};
PadDiff.prototype._extendChangesetWithAuthor = function(changeset, author, apool) {
//unpack
@@ -312,7 +312,7 @@ PadDiff.prototype._extendChangesetWithAuthor = function(changeset, author, apool
//return the modified changeset
return Changeset.pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank);
-}
+};
//this method is 80% like Changeset.inverse. I just changed so instead of reverting, it adds deletions and attribute changes to to the atext.
PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) {
@@ -331,14 +331,6 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) {
}
}
- function lines_length() {
- if ((typeof lines.length) == "number") {
- return lines.length;
- } else {
- return lines.length();
- }
- }
-
function alines_get(idx) {
if (alines.get) {
return alines.get(idx);
@@ -347,14 +339,6 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) {
}
}
- function alines_length() {
- if ((typeof alines.length) == "number") {
- return alines.length;
- } else {
- return alines.length();
- }
- }
-
var curLine = 0;
var curChar = 0;
var curLineOpIter = null;
@@ -463,7 +447,7 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) {
// If the text this operator applies to is only a star, than this is a false positive and should be ignored
if (csOp.attribs && textBank != "*") {
var deletedAttrib = apool.putAttrib(["removed", true]);
- var authorAttrib = apool.putAttrib(["author", ""]);;
+ var authorAttrib = apool.putAttrib(["author", ""]);
attribKeys.length = 0;
attribValues.length = 0;
@@ -473,7 +457,7 @@ PadDiff.prototype._createDeletionChangeset = function(cs, startAText, apool) {
if(apool.getAttribKey(n) === "author"){
authorAttrib = n;
- };
+ }
});
var undoBackToAttribs = cachedStrFunc(function (attribs) {
diff --git a/src/node/utils/randomstring.js b/src/node/utils/randomstring.js
index 4c1bba24..3815c66d 100644
--- a/src/node/utils/randomstring.js
+++ b/src/node/utils/randomstring.js
@@ -1,16 +1,11 @@
/**
* Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids
*/
-var randomString = function randomString(len)
+var crypto = require('crypto');
+
+var randomString = function(len)
{
- var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
- var randomstring = '';
- for (var i = 0; i < len; i++)
- {
- var rnum = Math.floor(Math.random() * chars.length);
- randomstring += chars.substring(rnum, rnum + 1);
- }
- return randomstring;
+ return crypto.randomBytes(len).toString('hex')
};
module.exports = randomString;
diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json
index b010f851..70001f8f 100644
--- a/src/node/utils/tar.json
+++ b/src/node/utils/tar.json
@@ -46,7 +46,6 @@
, "Changeset.js"
, "ChangesetUtils.js"
, "skiplist.js"
- , "virtual_lines.js"
, "cssmanager.js"
, "colorutils.js"
, "undomodule.js"
diff --git a/src/node/utils/toolbar.js b/src/node/utils/toolbar.js
new file mode 100644
index 00000000..a4ee202c
--- /dev/null
+++ b/src/node/utils/toolbar.js
@@ -0,0 +1,239 @@
+/**
+ * The Toolbar Module creates and renders the toolbars and buttons
+ */
+var _ = require("underscore")
+ , tagAttributes
+ , tag
+ , defaultButtons
+ , Button
+ , ButtonsGroup
+ , Separator
+ , defaultButtonAttributes;
+
+defaultButtonAttributes = function (name, overrides) {
+ return {
+ command: name,
+ localizationId: "pad.toolbar." + name + ".title",
+ class: "buttonicon buttonicon-" + name
+ };
+};
+
+tag = function (name, attributes, contents) {
+ var aStr = tagAttributes(attributes);
+
+ if (_.isString(contents) && contents.length > 0) {
+ return '<' + name + aStr + '>' + contents + '</' + name + '>';
+ }
+ else {
+ return '<' + name + aStr + '></' + name + '>';
+ }
+};
+
+tagAttributes = function (attributes) {
+ attributes = _.reduce(attributes || {}, function (o, val, name) {
+ if (!_.isUndefined(val)) {
+ o[name] = val;
+ }
+ return o;
+ }, {});
+
+ return " " + _.map(attributes, function (val, name) {
+ return "" + name + '="' + _.escape(val) + '"';
+ }).join(" ");
+};
+
+ButtonsGroup = function () {
+ this.buttons = [];
+};
+
+ButtonsGroup.fromArray = function (array) {
+ var btnGroup = new this;
+ _.each(array, function (btnName) {
+ btnGroup.addButton(Button.load(btnName));
+ });
+ return btnGroup;
+};
+
+ButtonsGroup.prototype.addButton = function (button) {
+ this.buttons.push(button);
+ return this;
+};
+
+ButtonsGroup.prototype.render = function () {
+ if (this.buttons.length == 1) {
+ this.buttons[0].grouping = "";
+ }
+ else {
+ _.first(this.buttons).grouping = "grouped-left";
+ _.last(this.buttons).grouping = "grouped-right";
+ _.each(this.buttons.slice(1, -1), function (btn) {
+ btn.grouping = "grouped-middle"
+ });
+ }
+
+ return _.map(this.buttons, function (btn) {
+ return btn.render();
+ }).join("\n");
+};
+
+Button = function (attributes) {
+ this.attributes = attributes;
+};
+
+Button.load = function (btnName) {
+ var button = module.exports.availableButtons[btnName];
+ if (button.constructor === Button || button.constructor === SelectButton) {
+ return button;
+ }
+ else {
+ return new Button(button);
+ }
+};
+
+_.extend(Button.prototype, {
+ grouping: "",
+
+ render: function () {
+ var liAttributes = {
+ "data-type": "button",
+ "data-key": this.attributes.command,
+ };
+ return tag("li", liAttributes,
+ tag("a", { "class": this.grouping },
+ tag("span", { "class": " "+ this.attributes.class, "data-l10n-id": this.attributes.localizationId })
+ )
+ );
+ }
+});
+
+SelectButton = function (attributes) {
+ this.attributes = attributes;
+ this.options = [];
+};
+
+_.extend(SelectButton.prototype, Button.prototype, {
+ addOption: function (value, text, attributes) {
+ this.options.push({
+ value: value,
+ text: text,
+ attributes: attributes
+ });
+ return this;
+ },
+
+ select: function (attributes) {
+ var self = this
+ , options = [];
+
+ _.each(this.options, function (opt) {
+ var a = _.extend({
+ value: opt.value
+ }, opt.attributes);
+
+ options.push( tag("option", a, opt.text) );
+ });
+ return tag("select", attributes, options.join(""));
+ },
+
+ render: function () {
+ var attributes = {
+ id: this.attributes.id,
+ "data-key": this.attributes.command,
+ "data-type": "select"
+ };
+ return tag("li", attributes,
+ this.select({ id: this.attributes.selectId })
+ );
+ }
+});
+
+Separator = function () {};
+Separator.prototype.render = function () {
+ return tag("li", { "class": "separator" });
+};
+
+module.exports = {
+ availableButtons: {
+ bold: defaultButtonAttributes("bold"),
+ italic: defaultButtonAttributes("italic"),
+ underline: defaultButtonAttributes("underline"),
+ strikethrough: defaultButtonAttributes("strikethrough"),
+
+ orderedlist: {
+ command: "insertorderedlist",
+ localizationId: "pad.toolbar.ol.title",
+ class: "buttonicon buttonicon-insertorderedlist"
+ },
+
+ unorderedlist: {
+ command: "insertunorderedlist",
+ localizationId: "pad.toolbar.ul.title",
+ class: "buttonicon buttonicon-insertunorderedlist"
+ },
+
+ indent: defaultButtonAttributes("indent"),
+ outdent: {
+ command: "outdent",
+ localizationId: "pad.toolbar.unindent.title",
+ class: "buttonicon buttonicon-outdent"
+ },
+
+ undo: defaultButtonAttributes("undo"),
+ redo: defaultButtonAttributes("redo"),
+
+ clearauthorship: {
+ command: "clearauthorship",
+ localizationId: "pad.toolbar.clearAuthorship.title",
+ class: "buttonicon buttonicon-clearauthorship"
+ },
+
+ importexport: {
+ command: "import_export",
+ localizationId: "pad.toolbar.import_export.title",
+ class: "buttonicon buttonicon-import_export"
+ },
+
+ timeslider: {
+ command: "showTimeSlider",
+ localizationId: "pad.toolbar.timeslider.title",
+ class: "buttonicon buttonicon-history"
+ },
+
+ savedrevision: defaultButtonAttributes("savedRevision"),
+ settings: defaultButtonAttributes("settings"),
+ embed: defaultButtonAttributes("embed"),
+ showusers: defaultButtonAttributes("showusers"),
+
+ timeslider_export: {
+ command: "import_export",
+ localizationId: "timeslider.toolbar.exportlink.title",
+ class: "buttonicon buttonicon-import_export"
+ },
+
+ timeslider_returnToPad: {
+ command: "timeslider_returnToPad",
+ localizationId: "timeslider.toolbar.returnbutton",
+ class: "buttontext"
+ }
+ },
+
+ registerButton: function (buttonName, buttonInfo) {
+ this.availableButtons[buttonName] = buttonInfo;
+ },
+
+ button: function (attributes) {
+ return new Button(attributes);
+ },
+ separator: function () {
+ return (new Separator).render();
+ },
+ selectButton: function (attributes) {
+ return new SelectButton(attributes);
+ },
+ menu: function (buttons) {
+ var groups = _.map(buttons, function (group) {
+ return ButtonsGroup.fromArray(group).render();
+ });
+ return groups.join(this.separator());
+ }
+};