diff --git a/bin/installOnWindows.bat b/bin/installOnWindows.bat
new file mode 100644
index 000000000..0e3e20996
--- /dev/null
+++ b/bin/installOnWindows.bat
@@ -0,0 +1,38 @@
+@echo off
+set NODE_VERSION=0.6.5
+set JQUERY_VERSION=1.7
+
+:: change directory to etherpad-lite root
+cd bin
+cd ..
+
+echo _
+echo Updating node...
+curl -lo bin\node.exe http://nodejs.org/dist/v%NODE_VERSION%/node.exe
+
+echo _
+echo Installing etherpad-lite and dependencies...
+cmd /C npm install src/
+
+echo _
+echo Updating jquery...
+curl -lo "node_modules\ep_etherpad-lite\static\js\jquery.min.js" "http://code.jquery.com/jquery-%JQUERY_VERSION%.min.js"
+
+echo _
+echo Copying custom templates...
+set custom_dir=node_modules\ep_etherpad-lite\static\custom
+FOR %%f IN (index pad timeslider) DO (
+ if NOT EXIST %custom_dir%\%%f.js copy %custom_dir%\js.template %custom_dir%\%%f.js
+ if NOT EXIST %custom_dir%\%%f.css copy %custom_dir%\css.template %custom_dir%\%%f.css
+)
+
+echo _
+echo Clearing cache.
+del /S var\minified*
+
+echo _
+echo Setting up settings.json...
+IF NOT EXIST settings.json copy settings.json.template_windows settings.json
+
+echo _
+echo Installed Etherpad-lite!
\ No newline at end of file
diff --git a/settings.json.template b/settings.json.template
index f89fcd8ed..7d175a34e 100644
--- a/settings.json.template
+++ b/settings.json.template
@@ -8,8 +8,8 @@
"ip": "0.0.0.0",
"port" : 9001,
- //The Type of the database. You can choose between dirty, sqlite and mysql
- //You should use mysql or sqlite for anything else than testing or development
+ //The Type of the database. You can choose between dirty, postgres, sqlite and mysql
+ //You shouldn't use "dirty" for for anything else than testing or development
"dbType" : "dirty",
//the database specific settings
"dbSettings" : {
diff --git a/src/node/db/API.js b/src/node/db/API.js
index 37fd3f161..661b78595 100644
--- a/src/node/db/API.js
+++ b/src/node/db/API.js
@@ -47,6 +47,8 @@ exports.createGroupPad = groupManager.createGroupPad;
exports.createAuthor = authorManager.createAuthor;
exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor;
+exports.listPadsOfAuthor = authorManager.listPadsOfAuthor;
+exports.padUsersCount = padMessageHandler.padUsersCount;
/**********************/
/**SESSION FUNCTIONS***/
@@ -282,6 +284,24 @@ exports.getRevisionsCount = function(padID, callback)
});
}
+/**
+getLastEdited(padID) returns the timestamp of the last revision of the pad
+
+Example returns:
+
+{code: 0, message:"ok", data: {lastEdited: 1340815946602}}
+{code: 1, message:"padID does not exist", data: null}
+*/
+exports.getLastEdited = function(padID, callback)
+{
+ //get the pad
+ getPadSafe(padID, true, function(err, pad)
+ {
+ if(ERR(err, callback)) return;
+ callback(null, {lastEdited: pad.getLastEdited()});
+ });
+}
+
/**
createPad(padName [, text]) creates a new pad in this group
@@ -463,6 +483,26 @@ exports.isPasswordProtected = function(padID, callback)
});
}
+/**
+listAuthorsOfPad(padID) returns an array of authors who contributed to this pad
+
+Example returns:
+
+{code: 0, message:"ok", data: {authorIDs : ["a.s8oes9dhwrvt0zif", "a.akf8finncvomlqva"]}
+{code: 1, message:"padID does not exist", data: null}
+*/
+exports.listAuthorsOfPad = function(padID, callback)
+{
+ //get the pad
+ getPadSafe(padID, true, function(err, pad)
+ {
+ if(ERR(err, callback)) return;
+
+ callback(null, {authorIDs: pad.getAllAuthors()});
+ });
+}
+
+
/******************************/
/** INTERNAL HELPER FUNCTIONS */
/******************************/
diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js
index f644de121..06b690518 100644
--- a/src/node/db/AuthorManager.js
+++ b/src/node/db/AuthorManager.js
@@ -55,6 +55,7 @@ exports.getAuthor4Token = function (token, callback)
/**
* Returns the AuthorID for a mapper.
* @param {String} token The mapper
+ * @param {String} name The name of the author (optional)
* @param {Function} callback callback (err, author)
*/
exports.createAuthorIfNotExistsFor = function (authorMapper, name, callback)
@@ -153,6 +154,7 @@ exports.getAuthorColorId = function (author, callback)
/**
* Sets the color Id of the author
* @param {String} author The id of the author
+ * @param {String} colorId The color id of the author
* @param {Function} callback (optional)
*/
exports.setAuthorColorId = function (author, colorId, callback)
@@ -173,9 +175,95 @@ exports.getAuthorName = function (author, callback)
/**
* Sets the name of the author
* @param {String} author The id of the author
+ * @param {String} name The name of the author
* @param {Function} callback (optional)
*/
exports.setAuthorName = function (author, name, callback)
{
db.setSub("globalAuthor:" + author, ["name"], name, callback);
}
+
+/**
+ * Returns an array of all pads this author contributed to
+ * @param {String} author The id of the author
+ * @param {Function} callback (optional)
+ */
+exports.listPadsOfAuthor = function (authorID, callback)
+{
+ /* There are two other places where this array is manipulated:
+ * (1) When the author is added to a pad, the author object is also updated
+ * (2) When a pad is deleted, each author of that pad is also updated
+ */
+ //get the globalAuthor
+ db.get("globalAuthor:" + authorID, function(err, author)
+ {
+ if(ERR(err, callback)) return;
+
+ //author does not exists
+ if(author == null)
+ {
+ callback(new customError("authorID does not exist","apierror"))
+ }
+ //everything is fine, return the pad IDs
+ else
+ {
+ var pads = [];
+ if(author.padIDs != null)
+ {
+ for (var padId in author.padIDs)
+ {
+ pads.push(padId);
+ }
+ }
+ callback(null, {padIDs: pads});
+ }
+ });
+}
+
+/**
+ * Adds a new pad to the list of contributions
+ * @param {String} author The id of the author
+ * @param {String} padID The id of the pad the author contributes to
+ */
+exports.addPad = function (authorID, padID)
+{
+ //get the entry
+ db.get("globalAuthor:" + authorID, function(err, author)
+ {
+ if(ERR(err)) return;
+ if(author == null) return;
+
+ //the entry doesn't exist so far, let's create it
+ if(author.padIDs == null)
+ {
+ author.padIDs = {};
+ }
+
+ //add the entry for this pad
+ author.padIDs[padID] = 1;// anything, because value is not used
+
+ //save the new element back
+ db.set("globalAuthor:" + authorID, author);
+ });
+}
+
+/**
+ * Removes a pad from the list of contributions
+ * @param {String} author The id of the author
+ * @param {String} padID The id of the pad the author contributes to
+ */
+exports.removePad = function (authorID, padID)
+{
+ db.get("globalAuthor:" + authorID, function (err, author)
+ {
+ if(ERR(err)) return;
+ if(author == null) return;
+
+ if(author.padIDs != null)
+ {
+ //remove pad from author
+ delete author.padIDs[padID];
+ db.set("globalAuthor:" + authorID, author);
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js
index b4a39c17e..ad2d59f38 100644
--- a/src/node/db/Pad.js
+++ b/src/node/db/Pad.js
@@ -80,8 +80,12 @@ Pad.prototype.appendRevision = function appendRevision(aChangeset, author) {
newRevData.meta.atext = this.atext;
}
- db.set("pad:"+this.id+":revs:"+newRev, newRevData);
+ db.set("pad:"+this.id+":revs:"+newRev, newRevData);
this.saveToDatabase();
+
+ // set the author to pad
+ if(author)
+ authorManager.addPad(author, this.id);
};
//save all attributes to the database
@@ -102,6 +106,12 @@ Pad.prototype.saveToDatabase = function saveToDatabase(){
db.set("pad:"+this.id, dbObject);
}
+// get time of last edit (changeset application)
+Pad.prototype.getLastEdit = function getLastEdit(callback){
+ var revNum = this.getHeadRevisionNumber();
+ db.getSub("pad:"+this.id+":revs:"+revNum, ["meta", "timestamp"], callback);
+}
+
Pad.prototype.getRevisionChangeset = function getRevisionChangeset(revNum, callback) {
db.getSub("pad:"+this.id+":revs:"+revNum, ["changeset"], callback);
};
@@ -436,6 +446,18 @@ Pad.prototype.remove = function remove(callback) {
db.remove("pad:"+padID+":revs:"+i);
}
+ callback();
+ },
+ //remove pad from all authors who contributed
+ function(callback)
+ {
+ var authorIDs = _this.getAllAuthors();
+
+ authorIDs.forEach(function (authorID)
+ {
+ authorManager.removePad(authorID, padID);
+ });
+
callback();
}
], callback);
diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js
index 98b1ed165..40c08441a 100644
--- a/src/node/handler/APIHandler.js
+++ b/src/node/handler/APIHandler.js
@@ -40,13 +40,14 @@ catch(e)
//a list of all functions
var functions = {
"createGroup" : [],
- "createGroupIfNotExistsFor" : ["groupMapper"],
+ "createGroupIfNotExistsFor" : ["groupMapper"],
"deleteGroup" : ["groupID"],
"listPads" : ["groupID"],
"createPad" : ["padID", "text"],
"createGroupPad" : ["groupID", "padName", "text"],
"createAuthor" : ["name"],
"createAuthorIfNotExistsFor": ["authorMapper" , "name"],
+ "listPadsOfAuthor" : ["authorID"],
"createSession" : ["groupID", "authorID", "validUntil"],
"deleteSession" : ["sessionID"],
"getSessionInfo" : ["sessionID"],
@@ -57,12 +58,15 @@ var functions = {
"getHTML" : ["padID", "rev"],
"setHTML" : ["padID", "html"],
"getRevisionsCount" : ["padID"],
+ "getLastEdited" : ["padID"],
"deletePad" : ["padID"],
"getReadOnlyID" : ["padID"],
"setPublicStatus" : ["padID", "publicStatus"],
"getPublicStatus" : ["padID"],
"setPassword" : ["padID", "password"],
- "isPasswordProtected" : ["padID"]
+ "isPasswordProtected" : ["padID"],
+ "listAuthorsOfPad" : ["padID"],
+ "padUsersCount" : ["padID"]
};
/**
diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js
index 8d2ca6cd3..9cf396691 100644
--- a/src/node/handler/PadMessageHandler.js
+++ b/src/node/handler/PadMessageHandler.js
@@ -33,6 +33,7 @@ var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins.js");
var log4js = require('log4js');
var messageLogger = log4js.getLogger("message");
var _ = require('underscore');
+var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js");
/**
* A associative array that saves which sessions belong to a pad
@@ -158,6 +159,11 @@ exports.handleDisconnect = function(client)
*/
exports.handleMessage = function(client, message)
{
+ _.map(hooks.callAll( "handleMessage", { client: client, message: message }), function ( newmessage ) {
+ if ( newmessage || newmessage === null ) {
+ message = newmessage;
+ }
+ });
if(message == null)
{
messageLogger.warn("Message is null!");
@@ -168,31 +174,65 @@ exports.handleMessage = function(client, message)
messageLogger.warn("Message has no type attribute!");
return;
}
-
- //Check what type of message we get and delegate to the other methodes
- if(message.type == "CLIENT_READY") {
- handleClientReady(client, message);
- } else if(message.type == "CHANGESET_REQ") {
- handleChangesetRequest(client, message);
- } else if(message.type == "COLLABROOM") {
- if (sessioninfos[client.id].readonly) {
- messageLogger.warn("Dropped message, COLLABROOM for readonly pad");
- } else if (message.data.type == "USER_CHANGES") {
- handleUserChanges(client, message);
- } else if (message.data.type == "USERINFO_UPDATE") {
- handleUserInfoUpdate(client, message);
- } else if (message.data.type == "CHAT_MESSAGE") {
- handleChatMessage(client, message);
- } else if (message.data.type == "SAVE_REVISION") {
- handleSaveRevisionMessage(client, message);
- } else if (message.data.type == "CLIENT_MESSAGE" &&
- message.data.payload.type == "suggestUserName") {
- handleSuggestUserName(client, message);
+
+ var finalHandler = function () {
+ //Check what type of message we get and delegate to the other methodes
+ if(message.type == "CLIENT_READY") {
+ handleClientReady(client, message);
+ } else if(message.type == "CHANGESET_REQ") {
+ handleChangesetRequest(client, message);
+ } else if(message.type == "COLLABROOM") {
+ if (sessioninfos[client.id].readonly) {
+ messageLogger.warn("Dropped message, COLLABROOM for readonly pad");
+ } else if (message.data.type == "USER_CHANGES") {
+ handleUserChanges(client, message);
+ } else if (message.data.type == "USERINFO_UPDATE") {
+ handleUserInfoUpdate(client, message);
+ } else if (message.data.type == "CHAT_MESSAGE") {
+ handleChatMessage(client, message);
+ } else if (message.data.type == "SAVE_REVISION") {
+ handleSaveRevisionMessage(client, message);
+ } else if (message.data.type == "CLIENT_MESSAGE" &&
+ message.data.payload.type == "suggestUserName") {
+ handleSuggestUserName(client, message);
+ } else {
+ messageLogger.warn("Dropped message, unknown COLLABROOM Data Type " + message.data.type);
+ }
} else {
- messageLogger.warn("Dropped message, unknown COLLABROOM Data Type " + message.data.type);
+ messageLogger.warn("Dropped message, unknown Message Type " + message.type);
}
+ };
+
+ if (message && message.padId) {
+ async.series([
+ //check permissions
+ function(callback)
+ {
+ // Note: message.sessionID is an entirely different kind of
+ // session from the sessions we use here! Beware! FIXME: Call
+ // our "sessions" "connections".
+ // FIXME: Use a hook instead
+ // FIXME: Allow to override readwrite access with readonly
+ securityManager.checkAccess(message.padId, message.sessionID, message.token, message.password, function(err, statusObject)
+ {
+ if(ERR(err, callback)) return;
+
+ //access was granted
+ if(statusObject.accessStatus == "grant")
+ {
+ callback();
+ }
+ //no access, send the client a message that tell him why
+ else
+ {
+ client.json.send({accessStatus: statusObject.accessStatus})
+ }
+ });
+ },
+ finalHandler
+ ]);
} else {
- messageLogger.warn("Dropped message, unknown Message Type " + message.type);
+ finalHandler();
}
}
@@ -1308,3 +1348,14 @@ function composePadChangesets(padId, startNum, endNum, callback)
callback(null, changeset);
});
}
+
+/**
+ * Get the number of users in a pad
+ */
+exports.padUsersCount = function (padID, callback) {
+ if (!pad2sessions[padID] || typeof pad2sessions[padID] != typeof []) {
+ callback(null, {padUsersCount: 0});
+ } else {
+ callback(null, {padUsersCount: pad2sessions[padID].length});
+ }
+}
diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js
index 4f5dd7a5d..24ec2c3d0 100644
--- a/src/node/hooks/express/padurlsanitize.js
+++ b/src/node/hooks/express/padurlsanitize.js
@@ -1,4 +1,5 @@
var padManager = require('../../db/PadManager');
+var url = require('url');
exports.expressCreateServer = function (hook_name, args, cb) {
//redirects browser to the pad's sanitized url if needed. otherwise, renders the html
@@ -14,9 +15,11 @@ exports.expressCreateServer = function (hook_name, args, cb) {
//the pad id was sanitized, so we redirect to the sanitized version
if(sanitizedPadId != padId)
{
- var real_path = req.path.replace(/^\/p\/[^\/]+/, '/p/' + sanitizedPadId);
- res.header('Location', real_path);
- res.send('You should be redirected to ' + real_path + '', 302);
+ var real_url = sanitizedPadId;
+ var query = url.parse(req.url).query;
+ if ( query ) real_url += '?' + query;
+ res.header('Location', real_url);
+ res.send('You should be redirected to ' + real_url + '', 302);
}
//the pad id was fine, so just render it
else
diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js
index 4b50b0326..7c638fb8c 100644
--- a/src/node/utils/ImportHtml.js
+++ b/src/node/utils/ImportHtml.js
@@ -20,7 +20,6 @@ var log4js = require('log4js');
var Changeset = require("ep_etherpad-lite/static/js/Changeset");
var contentcollector = require("ep_etherpad-lite/static/js/contentcollector");
-var map = require("ep_etherpad-lite/static/js/ace2_common").map;
function setPadHTML(pad, html, callback)
{
@@ -50,9 +49,7 @@ function setPadHTML(pad, html, callback)
}
// Get the new plain text and its attributes
- var newText = map(result.lines, function (e) {
- return e + '\n';
- }).join('');
+ var newText = result.lines.join('\n');
apiLogger.debug('newText:');
apiLogger.debug(newText);
var newAttribs = result.lineAttribs.join('|1+1') + '|1+1';
@@ -62,7 +59,7 @@ function setPadHTML(pad, html, callback)
var attribsIter = Changeset.opIterator(attribs);
var textIndex = 0;
var newTextStart = 0;
- var newTextEnd = newText.length - 1;
+ var newTextEnd = newText.length;
while (attribsIter.hasNext())
{
var op = attribsIter.next();
diff --git a/src/package.json b/src/package.json
index c46abbbf6..48750fbcb 100644
--- a/src/package.json
+++ b/src/package.json
@@ -25,7 +25,7 @@
"log4js" : "0.4.1",
"jsdom-nocontextifiy" : "0.2.10",
"async-stacktrace" : "0.0.2",
- "npm" : "1.1",
+ "npm" : "1.1.24",
"ejs" : "0.6.1",
"graceful-fs" : "1.1.5",
"slide" : "1.1.3",
diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js
index 723d410f2..cc9f1288d 100644
--- a/src/static/js/ace2_inner.js
+++ b/src/static/js/ace2_inner.js
@@ -341,6 +341,11 @@ function Ace2Inner(){
return rep;
};
+ editorInfo.ace_getAuthor = function()
+ {
+ return thisAuthor;
+ }
+
var currentCallStack = null;
function inCallStack(type, action)
@@ -439,6 +444,14 @@ function Ace2Inner(){
try
{
result = action();
+
+ hooks.callAll('aceEditEvent', {
+ callstack: currentCallStack,
+ editorInfo: editorInfo,
+ rep: rep,
+ documentAttributeManager: documentAttributeManager
+ });
+
//console.log("Just did action for: "+type);
cleanExit = true;
}
@@ -522,6 +535,7 @@ function Ace2Inner(){
{
return rep.lines.atOffset(charOffset).key;
}
+
function dispose()
{
diff --git a/src/static/js/chat.js b/src/static/js/chat.js
index 23b476675..47b0ae3ca 100644
--- a/src/static/js/chat.js
+++ b/src/static/js/chat.js
@@ -114,9 +114,13 @@ var chat = (function()
{
var count = Number($("#chatcounter").text());
count++;
+
+ // is the users focus already in the chatbox?
+ var alreadyFocused = $("#chatinput").is(":focus");
+
$("#chatcounter").text(count);
// chat throb stuff -- Just make it throw for twice as long
- if(wasMentioned)
+ if(wasMentioned && !alreadyFocused)
{ // If the user was mentioned show for twice as long and flash the browser window
if (chatMentions == 0){
title = document.title;
@@ -130,7 +134,11 @@ var chat = (function()
$('#chatthrob').html(""+authorName+"" + ": " + text).show().delay(2000).hide(400);
}
}
-
+ // Clear the chat mentions when the user clicks on the chat input box
+ $('#chatinput').click(function(){
+ chatMentions = 0;
+ document.title = title;
+ });
self.scrollDown();
},
diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js
index 1d4862230..7efcb49c2 100644
--- a/src/static/js/pluginfw/plugins.js
+++ b/src/static/js/pluginfw/plugins.js
@@ -54,10 +54,22 @@ exports.formatHooks = function (hook_set_name) {
};
exports.loadFn = function (path, hookName) {
- var x = path.split(":");
- var fn = require(x[0]);
- var functionName = x[1] ? x[1] : hookName;
+ var functionName
+ , parts = path.split(":");
+ // on windows: C:\foo\bar:xyz
+ if(parts[0].length == 1) {
+ if(parts.length == 3)
+ functionName = parts.pop();
+ path = parts.join(":");
+ }else{
+ path = parts[0];
+ functionName = parts[1];
+ }
+
+ var fn = require(path);
+ functionName = functionName ? functionName : hookName;
+
_.each(functionName.split("."), function (name) {
fn = fn[name];
});
diff --git a/src/templates/pad.html b/src/templates/pad.html
index 97aaa817f..02af9b107 100644
--- a/src/templates/pad.html
+++ b/src/templates/pad.html
@@ -320,6 +320,8 @@
+
+