resolve merge conflict

This commit is contained in:
John McLear 2013-12-09 21:55:04 +00:00
commit 58bbfd8a65
210 changed files with 12113 additions and 7505 deletions

View file

@ -74,6 +74,124 @@ exports.listSessionsOfAuthor = sessionManager.listSessionsOfAuthor;
/**PAD CONTENT FUNCTIONS*/
/************************/
/**
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
{
callback(null, {"changeset": pad.getRevisionChangeset(pad.getHeadRevisionNumber())});
}
});
}
/**
getText(padID, [rev]) returns the text of a pad
@ -326,8 +444,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)
@ -438,6 +556,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
@ -664,7 +822,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){

View file

@ -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);
}
});
}
}

View file

@ -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});
}
});
}

View file

@ -13,6 +13,8 @@ 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");
@ -404,6 +406,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 +635,7 @@ Pad.prototype.remove = function remove(callback) {
authorIDs.forEach(function (authorID)
{
authorManager.removePad(authorID, padID);
authorManager.removePad(authorID, padID);
});
callback();

View file

@ -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
@ -146,25 +164,23 @@ exports.getPad = function(id, text, callback)
else
{
pad = new Pad(id);
//initalize the pad
pad.init(text, function(err)
{
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
@ -230,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);
}

View file

@ -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
});
});
}

View file

@ -27,6 +27,8 @@ 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 +41,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 +124,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 +253,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 +289,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 +301,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"};
}

View file

@ -1,5 +1,5 @@
/**
* The Session Manager provides functions to manage session in the database
* The Session Manager provides functions to manage session in the database, it only provides session management for sessions created by the API
*/
/*

View file

@ -0,0 +1,82 @@
/*
* Stores session data in the database
* Source; https://github.com/edy-b/SciFlowWriter/blob/develop/available_plugins/ep_sciflowwriter/db/DirtyStore.js
* This is not used for authors that are created via the API at current
*/
var Store = require('ep_etherpad-lite/node_modules/connect/lib/middleware/session/store'),
utils = require('ep_etherpad-lite/node_modules/connect/lib/utils'),
Session = require('ep_etherpad-lite/node_modules/connect/lib/middleware/session/session'),
db = require('ep_etherpad-lite/node/db/DB').db,
log4js = require('ep_etherpad-lite/node_modules/log4js'),
messageLogger = log4js.getLogger("SessionStore");
var SessionStore = module.exports = function SessionStore() {};
SessionStore.prototype.__proto__ = Store.prototype;
SessionStore.prototype.get = function(sid, fn){
messageLogger.debug('GET ' + sid);
var self = this;
db.get("sessionstorage:" + sid, function (err, sess)
{
if (sess) {
sess.cookie.expires = 'string' == typeof sess.cookie.expires ? new Date(sess.cookie.expires) : sess.cookie.expires;
if (!sess.cookie.expires || new Date() < sess.cookie.expires) {
fn(null, sess);
} else {
self.destroy(sid, fn);
}
} else {
fn();
}
});
};
SessionStore.prototype.set = function(sid, sess, fn){
messageLogger.debug('SET ' + sid);
db.set("sessionstorage:" + sid, sess);
process.nextTick(function(){
if(fn) fn();
});
};
SessionStore.prototype.destroy = function(sid, fn){
messageLogger.debug('DESTROY ' + sid);
db.remove("sessionstorage:" + sid);
process.nextTick(function(){
if(fn) fn();
});
};
SessionStore.prototype.all = function(fn){
messageLogger.debug('ALL');
var sessions = [];
db.forEach(function(key, value){
if (key.substr(0,15) === "sessionstorage:") {
sessions.push(value);
}
});
fn(null, sessions);
};
SessionStore.prototype.clear = function(fn){
messageLogger.debug('CLEAR');
db.forEach(function(key, value){
if (key.substr(0,15) === "sessionstorage:") {
db.db.remove("session:" + key);
}
});
if(fn) fn();
};
SessionStore.prototype.length = function(fn){
messageLogger.debug('LENGTH');
var i = 0;
db.forEach(function(key, value){
if (key.substr(0,15) === "sessionstorage:") {
i++;
}
});
fn(null, i);
};

View file

@ -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,8 +214,100 @@ 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"]
}
};
// set the latest available API version here
exports.latestApiVersion = '1.2.9';
// exports the versions so it can be used by the new Swagger endpoint
exports.version = version;
/**
* Handles a HTTP API call
* @param functionName the name of the called function
@ -235,7 +327,7 @@ exports.handle = function(apiVersion, functionName, fields, req, res)
break;
}
}
//say goodbye if this is an unkown API version
if(!isKnownApiVersion)
{
@ -243,7 +335,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])
@ -254,15 +346,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});
@ -296,21 +390,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
@ -325,7 +417,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);
}

View file

@ -20,6 +20,7 @@
var ERR = require("async-stacktrace");
var exporthtml = require("../utils/ExportHtml");
var exporttxt = require("../utils/ExportTxt");
var exportdokuwiki = require("../utils/ExportDokuWiki");
var padManager = require("../db/PadManager");
var async = require("async");
@ -48,22 +49,75 @@ exports.doExport = function(req, res, padId, type)
res.attachment(padId + "." + 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")
{
padManager.getPad(padId, function(err, pad)
{
ERR(err);
if(req.params.rev){
pad.getInternalRevisionAText(req.params.rev, function(junk, text)
{
res.send(text.text ? text.text : null);
});
}
else
var txt;
var randNum;
var srcFile, destFile;
async.series([
//render the txt document
function(callback)
{
res.send(pad.text());
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([
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);
}
});
], function(err)
{
if(err && err != "stop") ERR(err);
})
}
else if(type == 'dokuwiki')
{

View file

@ -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,69 @@ 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);
}
},
//convert file to html
function(callback) {
var randNum = Math.floor(Math.random()*0xFFFFFFFF);
function(callback){
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 {
callback();
}
});
} else {
// if no abiword only rename
fs.rename(srcFile, destFile, callback);
// 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) {
if(!importHandledByPlugin){
if (abiword) {
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 {
callback();
}
},
//get the pad object
function(callback) {
padManager.getPad(padId, function(err, _pad){
@ -127,7 +171,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 +189,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 +227,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);
});
}

View file

@ -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]={};
}
@ -92,12 +107,23 @@ exports.kickSessionsFromPad = function(padID)
*/
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)
{
@ -122,10 +148,6 @@ exports.handleDisconnect = function(client)
});
}
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];
}
@ -137,26 +159,25 @@ exports.handleDisconnect = function(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;
}
if(!sessioninfos[client.id]) {
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
// 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;
@ -178,7 +199,8 @@ exports.handleMessage = function(client, message)
if (sessioninfos[client.id].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 +227,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")
// 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)
{
callback();
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);
}
//no access, send the client a message that tell him why
else
{
client.json.send({accessStatus: statusObject.accessStatus})
}
});
}
},
finalHandler
]);
@ -254,6 +306,25 @@ function handleSaveRevisionMessage(client, message){
});
}
/**
* 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)
*
@ -384,7 +455,7 @@ function handleGetChatMessages(client, message)
pad.getChatMessages(start, end, function(err, chatMessages)
{
if(ERR(err, callback)) return;
var infoMsg = {
type: "COLLABROOM",
data: {
@ -392,7 +463,7 @@ function handleGetChatMessages(client, message)
messages: chatMessages
}
};
// send the messages back to the client
client.json.send(infoMsg);
});
@ -493,23 +564,29 @@ 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();
}
//get all Vars we need
@ -521,6 +598,9 @@ function handleUserChanges(client, message)
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
@ -547,23 +627,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 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 it failed checkRep");
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;
@ -586,7 +680,14 @@ function handleUserChanges(client, message)
// client) are relative to revision r - 1. The follow function
// rebases "changeset" so that it is relative to revision r
// and can be applied after "c".
changeset = Changeset.follow(c, changeset, false, apool);
try
{
changeset = Changeset.follow(c, changeset, false, apool);
}catch(e){
client.json.send({disconnect:"badChangeset"});
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
async.nextTick(callback);
@ -606,10 +707,9 @@ function handleUserChanges(client, message)
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);
@ -625,11 +725,16 @@ function handleUserChanges(client, message)
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)
});
}
@ -713,7 +818,7 @@ exports.updatePadClients = function(pad, 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;
@ -882,8 +987,7 @@ 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);
@ -917,6 +1021,11 @@ function handleClientReady(client, message)
//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');
}
@ -928,17 +1037,25 @@ function handleClientReady(client, message)
//If this is a reconnect, we don't have to send the client the ClientVars again
if(message.reconnect == true)
{
//Join the pad and start receiving updates
client.join(padIds.padId);
//Save the revision in sessioninfos, we take the revision from the info the client send to us
sessioninfos[client.id].rev = message.client_rev;
}
//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
@ -959,7 +1076,6 @@ function handleClientReady(client, message)
"historicalAuthorData": historicalAuthorData,
"apool": apool,
"rev": pad.getHeadRevisionNumber(),
"globalPadId": message.padId,
"time": currentTime,
},
"colorPalette": authorManager.getColorPalette(),
@ -976,7 +1092,6 @@ function handleClientReady(client, message)
"readOnlyId": padIds.readOnlyPadId,
"readonly": padIds.readonly,
"serverTimestamp": new Date().getTime(),
"globalPadId": message.padId,
"userId": author,
"abiwordAvailable": settings.abiwordAvailable(),
"plugins": {
@ -1035,7 +1150,7 @@ function handleClientReady(client, message)
}
// notify all existing users about new user
client.broadcast.to(padIds.padIds).json.send(messageToTheOtherUsers);
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)
@ -1429,7 +1544,7 @@ function composePadChangesets(padId, startNum, endNum, callback)
*/
exports.padUsersCount = function (padID, callback) {
callback(null, {
padUsersCount: socketio.sockets.clients(padId).length
padUsersCount: socketio.sockets.clients(padID).length
});
}
@ -1439,7 +1554,7 @@ exports.padUsersCount = function (padID, callback) {
exports.padUsers = function (padID, callback) {
var result = [];
async.forEach(socketio.sockets.clients(padId), function(roomClient, callback) {
async.forEach(socketio.sockets.clients(padID), function(roomClient, callback) {
var s = sessioninfos[roomClient.id];
if(s) {
authorManager.getAuthor(s.author, function(err, author) {
@ -1447,6 +1562,7 @@ exports.padUsers = function (padID, callback) {
author.id = s.author;
result.push(author);
callback();
});
}
}, function(err) {
@ -1455,3 +1571,5 @@ exports.padUsers = function (padID, callback) {
callback(null, {padUsers: result});
});
}
exports.sessioninfos = sessioninfos;

View file

@ -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)

View file

@ -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)
{
@ -27,11 +27,11 @@ exports.createServer = function () {
}
console.log("Report bugs at https://github.com/ether/etherpad-lite/issues")
serverName = "Etherpad-Lite " + version + " (http://etherpad.org)";
serverName = "Etherpad " + version + " (http://etherpad.org)";
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");
}
@ -75,6 +75,10 @@ exports.restartServer = function () {
next();
});
if(settings.trustProxy){
app.enable('trust proxy');
}
app.configure(function() {
hooks.callAll("expressConfigure", {"app": app});
});

View file

@ -2,6 +2,7 @@ var eejs = require('ep_etherpad-lite/node/eejs');
exports.expressCreateServer = function (hook_name, args, cb) {
args.app.get('/admin', function(req, res) {
if('/' != req.path[req.path.length-1]) return res.redirect('/admin/');
res.send( eejs.require("ep_etherpad-lite/templates/admin/index.html", {}) );
});
}

View file

@ -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;
})
}

View file

@ -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,8 +54,18 @@ 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");
});
});
//Provide a possibility to query the latest available API version
args.app.get('/api', function (req, res) {
res.json({"currentVersion" : apiHandler.latestApiVersion});
});
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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);
});
});
}
}

View file

@ -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();
}
});
}
});

View file

@ -2,6 +2,10 @@ var path = require('path');
var eejs = require('ep_etherpad-lite/node/eejs');
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)
@ -45,11 +49,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);
}
});
});
}
}

View file

@ -0,0 +1,430 @@
var log4js = require('log4js');
var express = require('express');
var apiHandler = require('../../handler/APIHandler');
var apiCaller = require('./apicalls').apiCaller;
var settings = require("../../utils/Settings");
var swaggerModels = {
'models': {
'SessionInfo' : {
"id": 'SessionInfo',
"properties": {
"id": {
"type": "string"
},
"authorID": {
"type": "string"
},
"groupID":{
"type":"string"
},
"validUntil":{
"type":"long"
}
}
},
'UserInfo' : {
"id": 'UserInfo',
"properties": {
"id": {
"type": "string"
},
"colorId": {
"type": "string"
},
"name":{
"type":"string"
},
"timestamp":{
"type":"long"
}
}
},
'Message' : {
"id": 'Message',
"properties": {
"text": {
"type": "string"
},
"userId": {
"type": "string"
},
"userName":{
"type":"string"
},
"time":{
"type":"long"
}
}
}
}
};
function sessionListResponseProcessor(res) {
if (res.data) {
var sessions = [];
for (var sessionId in res.data) {
var sessionInfo = res.data[sessionId];
sessionId["id"] = sessionId;
sessions.push(sessionInfo);
}
res.data = sessions;
}
return res;
}
// We'll add some more info to the API methods
var API = {
// Group
"group": {
"create" : {
"func" : "createGroup",
"description": "creates a new group",
"response": {"groupID":{"type":"string"}}
},
"createIfNotExistsFor" : {
"func": "createGroupIfNotExistsFor",
"description": "this functions helps you to map your application group ids to Etherpad group ids",
"response": {"groupID":{"type":"string"}}
},
"delete" : {
"func": "deleteGroup",
"description": "deletes a group"
},
"listPads" : {
"func": "listPads",
"description": "returns all pads of this group",
"response": {"padIDs":{"type":"List", "items":{"type":"string"}}}
},
"createPad" : {
"func": "createGroupPad",
"description": "creates a new pad in this group"
},
"listSessions": {
"func": "listSessionsOfGroup",
"responseProcessor": sessionListResponseProcessor,
"description": "",
"response": {"sessions":{"type":"List", "items":{"type":"SessionInfo"}}}
},
"list": {
"func": "listAllGroups",
"description": "",
"response": {"groupIDs":{"type":"List", "items":{"type":"string"}}}
},
},
// Author
"author": {
"create" : {
"func" : "createAuthor",
"description": "creates a new author",
"response": {"authorID":{"type":"string"}}
},
"createIfNotExistsFor": {
"func": "createAuthorIfNotExistsFor",
"description": "this functions helps you to map your application author ids to Etherpad author ids",
"response": {"authorID":{"type":"string"}}
},
"listPads": {
"func": "listPadsOfAuthor",
"description": "returns an array of all pads this author contributed to",
"response": {"padIDs":{"type":"List", "items":{"type":"string"}}}
},
"listSessions": {
"func": "listSessionsOfAuthor",
"responseProcessor": sessionListResponseProcessor,
"description": "returns all sessions of an author",
"response": {"sessions":{"type":"List", "items":{"type":"SessionInfo"}}}
},
// We need an operation that return a UserInfo so it can be picked up by the codegen :(
"getName" : {
"func": "getAuthorName",
"description": "Returns the Author Name of the author",
"responseProcessor": function(response) {
if (response.data) {
response["info"] = {"name": response.data.authorName};
delete response["data"];
}
},
"response": {"info":{"type":"UserInfo"}}
}
},
"session": {
"create" : {
"func": "createSession",
"description": "creates a new session. validUntil is an unix timestamp in seconds",
"response": {"sessionID":{"type":"string"}}
},
"delete" : {
"func": "deleteSession",
"description": "deletes a session"
},
// We need an operation that returns a SessionInfo so it can be picked up by the codegen :(
"info": {
"func": "getSessionInfo",
"description": "returns informations about a session",
"responseProcessor": function(response) {
// move this to info
if (response.data) {
response["info"] = response.data;
delete response["data"];
}
},
"response": {"info":{"type":"SessionInfo"}}
}
},
"pad": {
"listAll" : {
"func": "listAllPads",
"description": "list all the pads",
"response": {"padIDs":{"type":"List", "items": {"type" : "string"}}}
},
"createDiffHTML" : {
"func" : "createDiffHTML",
"description": "",
"response": {}
},
"create" : {
"func" : "createPad",
"description": "creates a new (non-group) pad. Note that if you need to create a group Pad, you should call createGroupPad"
},
"getText" : {
"func" : "getText",
"description": "returns the text of a pad",
"response": {"text":{"type":"string"}}
},
"setText" : {
"func" : "setText",
"description": "sets the text of a pad"
},
"getHTML": {
"func" : "getHTML",
"description": "returns the text of a pad formatted as HTML",
"response": {"html":{"type":"string"}}
},
"setHTML": {
"func" : "setHTML",
"description": "sets the text of a pad with HTML"
},
"getRevisionsCount": {
"func" : "getRevisionsCount",
"description": "returns the number of revisions of this pad",
"response": {"revisions":{"type":"long"}}
},
"getLastEdited": {
"func" : "getLastEdited",
"description": "returns the timestamp of the last revision of the pad",
"response": {"lastEdited":{"type":"long"}}
},
"delete": {
"func" : "deletePad",
"description": "deletes a pad"
},
"getReadOnlyID": {
"func" : "getReadOnlyID",
"description": "returns the read only link of a pad",
"response": {"readOnlyID":{"type":"string"}}
},
"setPublicStatus": {
"func": "setPublicStatus",
"description": "sets a boolean for the public status of a pad"
},
"getPublicStatus": {
"func": "getPublicStatus",
"description": "return true of false",
"response": {"publicStatus":{"type":"boolean"}}
},
"setPassword": {
"func": "setPassword",
"description": "returns ok or a error message"
},
"isPasswordProtected": {
"func": "isPasswordProtected",
"description": "returns true or false",
"response": {"passwordProtection":{"type":"boolean"}}
},
"authors": {
"func": "listAuthorsOfPad",
"description": "returns an array of authors who contributed to this pad",
"response": {"authorIDs":{"type":"List", "items":{"type" : "string"}}}
},
"usersCount": {
"func": "padUsersCount",
"description": "returns the number of user that are currently editing this pad",
"response": {"padUsersCount":{"type": "long"}}
},
"users": {
"func": "padUsers",
"description": "returns the list of users that are currently editing this pad",
"response": {"padUsers":{"type":"List", "items":{"type": "UserInfo"}}}
},
"sendClientsMessage": {
"func": "sendClientsMessage",
"description": "sends a custom message of type msg to the pad"
},
"checkToken" : {
"func": "checkToken",
"description": "returns ok when the current api token is valid"
},
"getChatHistory": {
"func": "getChatHistory",
"description": "returns the chat history",
"response": {"messages":{"type":"List", "items": {"type" : "Message"}}}
},
// We need an operation that returns a Message so it can be picked up by the codegen :(
"getChatHead": {
"func": "getChatHead",
"description": "returns the chatHead (chat-message) of the pad",
"responseProcessor": function(response) {
// move this to info
if (response.data) {
response["chatHead"] = {"time": response.data["chatHead"]};
delete response["data"];
}
},
"response": {"chatHead":{"type":"Message"}}
}
}
};
function capitalise(string){
return string.charAt(0).toUpperCase() + string.slice(1);
}
for (var resource in API) {
for (var func in API[resource]) {
// The base response model
var responseModel = {
"properties": {
"code":{
"type":"int"
},
"message":{
"type":"string"
}
}
};
var responseModelId = "Response";
// Add the data properties (if any) to the response
if (API[resource][func]["response"]) {
// This is a specific response so let's set a new id
responseModelId = capitalise(resource) + capitalise(func) + "Response";
for(var prop in API[resource][func]["response"]) {
var propType = API[resource][func]["response"][prop];
responseModel["properties"][prop] = propType;
}
}
// Add the id
responseModel["id"] = responseModelId;
// Add this to the swagger models
swaggerModels['models'][responseModelId] = responseModel;
// Store the response model id
API[resource][func]["responseClass"] = responseModelId;
}
}
function newSwagger() {
var swagger_module = require.resolve("swagger-node-express");
if (require.cache[swagger_module]) {
// delete the child modules from cache
require.cache[swagger_module].children.forEach(function(m) {delete require.cache[m.id];});
// delete the module from cache
delete require.cache[swagger_module];
}
return require("swagger-node-express");
}
exports.expressCreateServer = function (hook_name, args, cb) {
for (var version in apiHandler.version) {
var swagger = newSwagger();
var basePath = "/rest/" + version;
// Let's put this under /rest for now
var subpath = express();
args.app.use(basePath, subpath);
swagger.setAppHandler(subpath);
swagger.addModels(swaggerModels);
for (var resource in API) {
for (var funcName in API[resource]) {
var func = API[resource][funcName];
// get the api function
var apiFunc = apiHandler.version[version][func["func"]];
// Skip this one if it does not exist in the version
if(!apiFunc) {
continue;
}
var swaggerFunc = {
'spec': {
"description" : func["description"],
"path" : "/" + resource + "/" + funcName,
"summary" : funcName,
"nickname" : funcName,
"method": "GET",
"params" : apiFunc.map( function(param) {
return swagger.queryParam(param, param, "string");
}),
"responseClass" : func["responseClass"]
},
'action': (function(func, responseProcessor) {
return function (req,res) {
req.params.version = version;
req.params.func = func; // call the api function
//wrap the send function so we can process the response
res.__swagger_send = res.send;
res.send = function (response) {
// ugly but we need to get this as json
response = JSON.parse(response);
// process the response if needed
if (responseProcessor) {
response = responseProcessor(response);
}
// Let's move everything out of "data"
if (response.data) {
for(var prop in response.data) {
response[prop] = response.data[prop];
delete response.data;
}
}
response = JSON.stringify(response);
res.__swagger_send(response);
};
apiCaller(req, res, req.query);
};
})(func["func"], func["responseProcessor"]) // must use a closure here
};
swagger.addGet(swaggerFunc);
}
}
swagger.setHeaders = function setHeaders(res) {
res.header('Access-Control-Allow-Origin', "*");
};
swagger.configureSwaggerPaths("", "/api" , "");
swagger.configure("http://" + settings.ip + ":" + settings.port + basePath, version);
}
};

View file

@ -4,7 +4,8 @@ var httpLogger = log4js.getLogger("http");
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,8 +33,8 @@ 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;
@ -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
@ -102,15 +114,14 @@ exports.expressConfigure = function (hook_name, args, cb) {
* handling it cleaner :) */
if (!exports.sessionStore) {
exports.sessionStore = new express.session.MemoryStore();
exports.secret = randomString(32);
exports.sessionStore = new ueberStore();
exports.secret = settings.sessionKey; // Isn't this being reset each time the server spawns?
}
args.app.use(express.cookieParser(exports.secret));
args.app.use(express.cookieParser(exports.secret));
args.app.sessionStore = exports.sessionStore;
args.app.use(express.session({store: args.app.sessionStore,
key: 'express_sid' }));
args.app.use(express.session({secret: exports.secret, store: args.app.sessionStore, key: 'express_sid' }));
args.app.use(exports.basicAuth);
}

View file

@ -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();
},

3
src/node/stats.js Normal file
View file

@ -0,0 +1,3 @@
var measured = require('measured')
module.exports = measured.createCollection();

View file

@ -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});
};
}

View file

@ -316,7 +316,7 @@ exports.getPadDokuWikiDocument = function (padId, revNum, callback)
getPadDokuWiki(pad, revNum, callback);
});
}
};
function _escapeDokuWiki(s)
{

View file

@ -0,0 +1,87 @@
/**
* Helpers for export requests
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var async = require("async");
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
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');
exports.getPadPlainText = function(pad, revNum){
var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext());
var textLines = atext.text.slice(0, -1).split('\n');
var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
var apool = pad.pool();
var pieces = [];
for (var i = 0; i < textLines.length; i++){
var line = _analyzeLine(textLines[i], attribLines[i], apool);
if (line.listLevel){
var numSpaces = line.listLevel * 2 - 1;
var bullet = '*';
pieces.push(new Array(numSpaces + 1).join(' '), bullet, ' ', line.text, '\n');
}
else{
pieces.push(line.text, '\n');
}
}
return pieces.join('');
};
exports._analyzeLine = function(text, aline, apool){
var line = {};
// identify list
var lineMarker = 0;
line.listLevel = 0;
if (aline){
var opIter = Changeset.opIterator(aline);
if (opIter.hasNext()){
var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool);
if (listType){
lineMarker = 1;
listType = /([a-z]+)([12345678])/.exec(listType);
if (listType){
line.listTypeName = listType[1];
line.listLevel = Number(listType[2]);
}
}
}
}
if (lineMarker){
line.text = text.substring(1);
line.aline = Changeset.subattribution(aline, 1);
}
else{
line.text = text;
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) + ";";
});
};

View file

@ -21,31 +21,9 @@ 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');
function getPadPlainText(pad, revNum)
{
var atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext());
var textLines = atext.text.slice(0, -1).split('\n');
var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
var apool = pad.pool();
var pieces = [];
for (var i = 0; i < textLines.length; i++)
{
var line = _analyzeLine(textLines[i], attribLines[i], apool);
if (line.listLevel)
{
var numSpaces = line.listLevel * 2 - 1;
var bullet = '*';
pieces.push(new Array(numSpaces + 1).join(' '), bullet, ' ', line.text, '\n');
}
else
{
pieces.push(line.text, '\n');
}
}
return pieces.join('');
}
var getPadPlainText = require('./ExportHelper').getPadPlainText;
var _analyzeLine = require('./ExportHelper')._analyzeLine;
var _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
function getPadHTML(pad, revNum, callback)
{
@ -469,7 +447,7 @@ function getHTMLFromAtext(pad, atext, authorColors)
pieces.push('</li></ul>');
}
lists.length--;
}
}
var lineContentFromHook = hooks.callAllStr("getLineHTMLForExport",
{
line: line,
@ -477,14 +455,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>');
}
}
}
@ -503,45 +481,6 @@ function getHTMLFromAtext(pad, atext, authorColors)
return pieces.join('');
}
function _analyzeLine(text, aline, apool)
{
var line = {};
// identify list
var lineMarker = 0;
line.listLevel = 0;
if (aline)
{
var opIter = Changeset.opIterator(aline);
if (opIter.hasNext())
{
var listType = Changeset.opAttributeValue(opIter.next(), 'list', apool);
if (listType)
{
lineMarker = 1;
listType = /([a-z]+)([12345678])/.exec(listType);
if (listType)
{
line.listTypeName = listType[1];
line.listLevel = Number(listType[2]);
}
}
}
}
if (lineMarker)
{
line.text = text.substring(1);
line.aline = Changeset.subattribution(aline, 1);
}
else
{
line.text = text;
line.aline = aline;
}
return line;
}
exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback)
{
padManager.getPad(padId, function (err, pad)
@ -551,7 +490,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' +
@ -576,80 +515,7 @@ exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback)
callback(null, head + html + foot);
});
});
}
function _encodeWhitespace(s) {
return s.replace(/[^\x21-\x7E\s\t\n\r]/g, function(c)
{
return "&#" +c.charCodeAt(0) + ";"
});
}
// copied from ACE
function _processSpaces(s)
{
var doesWrap = true;
if (s.indexOf("<") < 0 && !doesWrap)
{
// short-cut
return s.replace(/ /g, '&nbsp;');
}
var parts = [];
s.replace(/<[^>]*>?| |[^ <]+/g, function (m)
{
parts.push(m);
});
if (doesWrap)
{
var endOfLine = true;
var beforeSpace = false;
// last space in a run is normal, others are nbsp,
// end of line is nbsp
for (var i = parts.length - 1; i >= 0; i--)
{
var p = parts[i];
if (p == " ")
{
if (endOfLine || beforeSpace) parts[i] = '&nbsp;';
endOfLine = false;
beforeSpace = true;
}
else if (p.charAt(0) != "<")
{
endOfLine = false;
beforeSpace = false;
}
}
// beginning of line is nbsp
for (var i = 0; i < parts.length; i++)
{
var p = parts[i];
if (p == " ")
{
parts[i] = '&nbsp;';
break;
}
else if (p.charAt(0) != "<")
{
break;
}
}
}
else
{
for (var i = 0; i < parts.length; i++)
{
var p = parts[i];
if (p == " ")
{
parts[i] = '&nbsp;';
}
}
}
return parts.join('');
}
};
// copied from ACE
@ -676,3 +542,56 @@ function _findURLs(text)
return urls;
}
// copied from ACE
function _processSpaces(s){
var doesWrap = true;
if (s.indexOf("<") < 0 && !doesWrap){
// short-cut
return s.replace(/ /g, '&nbsp;');
}
var parts = [];
s.replace(/<[^>]*>?| |[^ <]+/g, function (m){
parts.push(m);
});
if (doesWrap){
var endOfLine = true;
var beforeSpace = false;
// last space in a run is normal, others are nbsp,
// end of line is nbsp
for (var i = parts.length - 1; i >= 0; i--){
var p = parts[i];
if (p == " "){
if (endOfLine || beforeSpace) parts[i] = '&nbsp;';
endOfLine = false;
beforeSpace = true;
}
else if (p.charAt(0) != "<"){
endOfLine = false;
beforeSpace = false;
}
}
// beginning of line is nbsp
for (var i = 0; i < parts.length; i++){
var p = parts[i];
if (p == " "){
parts[i] = '&nbsp;';
break;
}
else if (p.charAt(0) != "<"){
break;
}
}
}
else
{
for (var i = 0; i < parts.length; i++){
var p = parts[i];
if (p == " "){
parts[i] = '&nbsp;';
}
}
}
return parts.join('');
}

292
src/node/utils/ExportTxt.js Normal file
View file

@ -0,0 +1,292 @@
/**
* TXT export
*/
/*
* 2013 John McLear
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var async = require("async");
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
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;
// This is slightly different than the HTML method as it passes the output to getTXTFromAText
function getPadTXT(pad, revNum, callback)
{
var atext = pad.atext;
var html;
async.waterfall([
// fetch revision atext
function (callback)
{
if (revNum != undefined)
{
pad.getInternalRevisionAText(revNum, function (err, revisionAtext)
{
if(ERR(err, callback)) return;
atext = revisionAtext;
callback();
});
}
else
{
callback(null);
}
},
// convert atext to html
function (callback)
{
html = getTXTFromAtext(pad, atext); // only this line is different to the HTML function
callback(null);
}],
// run final callback
function (err)
{
if(ERR(err, callback)) return;
callback(null, html);
});
}
exports.getPadTXT = getPadTXT;
// This is different than the functionality provided in ExportHtml as it provides formatting
// functionality that is designed specifically for TXT exports
function getTXTFromAtext(pad, atext, authorColors)
{
var apool = pad.apool();
var textLines = atext.text.slice(0, -1).split('\n');
var attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
var tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
var anumMap = {};
var css = "";
props.forEach(function (propName, i)
{
var propTrueNum = apool.putAttrib([propName, true], true);
if (propTrueNum >= 0)
{
anumMap[propTrueNum] = i;
}
});
function getLineTXT(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>
// becomes
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
var taker = Changeset.stringIterator(text);
var assem = Changeset.stringAssembler();
var openTags = [];
var idx = 0;
function processNextChars(numChars)
{
if (numChars <= 0)
{
return;
}
var iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars));
idx += numChars;
while (iter.hasNext())
{
var o = iter.next();
var propChanged = false;
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;
}
}
});
for (var i = 0; i < propVals.length; i++)
{
if (propVals[i] === true)
{
propVals[i] = LEAVE;
propChanged = true;
}
else if (propVals[i] === STAY)
{
propVals[i] = true; // set it back
}
}
// now each member of propVal is in {false,LEAVE,ENTER,true}
// according to what happens at start of span
if (propChanged)
{
// leaving bold (e.g.) also leaves italics, etc.
var left = false;
for (var i = 0; i < propVals.length; i++)
{
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
}
}
}
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);
}
}
for (var i = 0; i < propVals.length; i++)
{
if (propVals[i] === ENTER || propVals[i] === STAY)
{
propVals[i] = true;
}
}
// 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), "");
// remove * from s, it's just not needed on a blank line.. This stops
// plugins from being able to display * at the beginning of a line
// s = s.replace("*", ""); // Then remove it
assem.append(s);
} // end iteration over spans in line
var tags2close = [];
for (var i = propVals.length - 1; i >= 0; i--)
{
if (propVals[i])
{
tags2close.push(i);
propVals[i] = false;
}
}
} // end processNextChars
processNextChars(text.length - idx);
return(assem.toString());
} // end getLineHTML
var pieces = [css];
// Need to deal with constraints imposed on HTML lists; can
// only gain one level of nesting at once, can't change type
// mid-list, etc.
// People might use weird indenting, e.g. skip a level,
// so we want to do something reasonable there. We also
// want to deal gracefully with blank lines.
// => keeps track of the parents level of indentation
var lists = []; // e.g. [[1,'bullet'], [3,'bullet'], ...]
for (var i = 0; i < textLines.length; i++)
{
var line = _analyzeLine(textLines[i], attribLines[i], apool);
var lineContent = getLineTXT(line.text, line.aline);
if(line.listTypeName == "bullet"){
lineContent = "* " + lineContent; // add a bullet
}
if(line.listLevel > 0){
for (var j = line.listLevel - 1; j >= 0; j--){
pieces.push('\t');
}
if(line.listTypeName == "number"){
pieces.push(line.listLevel + ". ");
// This is bad because it doesn't truly reflect what the user
// sees because browsers do magic on nested <ol><li>s
}
pieces.push(lineContent, '\n');
}else{
pieces.push(lineContent, '\n');
}
}
return pieces.join('');
}
exports.getTXTFromAtext = getTXTFromAtext;
exports.getPadTXTDocument = function (padId, revNum, noDocType, callback)
{
padManager.getPad(padId, function (err, pad)
{
if(ERR(err, callback)) return;
getPadTXT(pad, revNum, function (err, html)
{
if(ERR(err, callback)) return;
callback(null, html);
});
});
};

View file

@ -26,7 +26,13 @@ function setPadHTML(pad, html, callback)
var apiLogger = log4js.getLogger("ImportHtml");
// 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);

View file

@ -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?)

View file

@ -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,8 +24,10 @@ 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;
/* Root path of the installation */
exports.root = path.normalize(path.join(npm.dir, ".."));
@ -33,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
@ -46,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
*/
@ -75,7 +77,7 @@ 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";
/**
* A flag that requires any user to have a valid session (via the api) before accessing a pad
@ -107,11 +109,26 @@ exports.abiword = null;
*/
exports.loglevel = "INFO";
/**
* Disable IP logging
*/
exports.disableIPlogging = false;
/*
* log4js appender configuration
*/
exports.logconfig = { appenders: [{ type: "console" }]};
/*
* Session Key, do not sure this.
*/
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 */
@ -130,14 +147,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{
@ -151,7 +166,8 @@ exports.reloadSettings = function reloadSettings() {
var settings;
try {
if(settingsStr) {
settings = vm.runInContext('exports = '+settingsStr, vm.createContext(), "settings.json");
settingsStr = jsonminify(settingsStr).replace(",]","]").replace(",}","}");
settings = JSON.parse(settingsStr);
}
}catch(e){
console.error('There was an error processing your settings.json file: '+e.message);
@ -179,15 +195,20 @@ 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();
if(exports.dbType === "dirty"){
console.warn("DirtyDB is used. This is fine for testing but not recommended for production.")
if(!exports.sessionKey){ // If the secretKey isn't set we also create yet another unique value here
exports.sessionKey = randomString(32);
console.warn("You need to set a sessionKey value in settings.json, this will allow your users to reconnect to your Etherpad Instance if your instance restarts");
}
}
if(exports.dbType === "dirty"){
console.warn("DirtyDB is used. This is fine for testing but not recommended for production.");
}
};
// initially load settings
exports.reloadSettings();

View file

@ -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();

View file

@ -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) {

View file

@ -46,7 +46,6 @@
, "Changeset.js"
, "ChangesetUtils.js"
, "skiplist.js"
, "virtual_lines.js"
, "cssmanager.js"
, "colorutils.js"
, "undomodule.js"