diff --git a/bin/installOnWindows.bat b/bin/installOnWindows.bat new file mode 100644 index 000000000..b6f699de8 --- /dev/null +++ b/bin/installOnWindows.bat @@ -0,0 +1,35 @@ +@echo off +set NODE_VERSION=0.6.5 +set JQUERY_VERSION=1.7 + +:: change directory to etherpad-lite root +cd bin +cd .. + +echo _ +echo Setting up settings.json... +copy settings.json.template_windows settings.json + +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 Some other stuff... +copy node_modules\ep_etherpad-lite\static\custom\js.template node_modules\ep_etherpad-lite\static\custom\index.template +copy node_modules\ep_etherpad-lite\static\custom\js.template node_modules\ep_etherpad-lite\static\custom\pad.template +copy node_modules\ep_etherpad-lite\static\custom\js.template node_modules\ep_etherpad-lite\static\custom\timeslider.template +copy node_modules\ep_etherpad-lite\static\custom\css.template node_modules\ep_etherpad-lite\static\custom\index.template +copy node_modules\ep_etherpad-lite\static\custom\css.template node_modules\ep_etherpad-lite\static\custom\pad.template +copy node_modules\ep_etherpad-lite\static\custom\css.template node_modules\ep_etherpad-lite\static\custom\timeslider.template + +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..e2b6f6f8b 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -47,6 +47,7 @@ exports.createGroupPad = groupManager.createGroupPad; exports.createAuthor = authorManager.createAuthor; exports.createAuthorIfNotExistsFor = authorManager.createAuthorIfNotExistsFor; +exports.listPadsOfAuthor = authorManager.listPadsOfAuthor; /**********************/ /**SESSION FUNCTIONS***/ @@ -282,6 +283,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 +482,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..567a90d28 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,14 @@ 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"] }; /** diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 8d2ca6cd3..b356230c7 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(); } } diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index 7b21206c9..fc274a075 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -16,6 +16,11 @@ exports.expressCreateServer = function (hook_name, args, cb) { "ep_etherpad-lite/templates/admin/plugins.html", render_args), {}); }); + args.app.get('/admin/plugins/info', function(req, res) { + res.send(eejs.require( + "ep_etherpad-lite/templates/admin/plugins-info.html", + {}), {}); + }); } exports.socketio = function (hook_name, args, cb) { diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js index 4f5dd7a5d..229d013df 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 = req.url.replace(/^\/p\/[^\/]+/, '/p/' + 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/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/hooks.js b/src/static/js/pluginfw/hooks.js index 49e46c608..7281cac96 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -102,7 +102,7 @@ exports.aCallAll = function (hook_name, args, cb) { exports.callFirst = function (hook_name, args) { if (!args) args = {}; - if (plugins.hooks[hook_name][0] === undefined) return []; + if (plugins.hooks[hook_name] === undefined) return []; return exports.syncMapFirst(plugins.hooks[hook_name], function (hook) { return hookCallWrapper(hook, hook_name, args); }); diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index 1f66da41c..455dd203b 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -41,28 +41,42 @@ exports.formatParts = function () { return _.map(exports.parts, function (part) { return part.full_name; }).join("\n"); }; -exports.formatHooks = function () { +exports.formatHooks = function (hook_set_name) { var res = []; - _.chain(exports.hooks).keys().forEach(function (hook_name) { - _.forEach(exports.hooks[hook_name], function (hook) { - res.push(hook.hook_name + ": " + hook.hook_fn_name + " from " + hook.part.full_name); + var hooks = exports.extractHooks(exports.parts, hook_set_name || "hooks"); + + _.chain(hooks).keys().forEach(function (hook_name) { + _.forEach(hooks[hook_name], function (hook) { + res.push("
" + hook.hook_name + "
" + hook.hook_fn_name + " from " + hook.part.full_name + "
"); }); }); - return res.join("\n"); + return "
" + res.join("\n") + "
"; }; 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 + if(process.platform == 'win32') { + 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]; }); return fn; }; -exports.extractHooks = function (parts, hook_set_name, plugins) { +exports.extractHooks = function (parts, hook_set_name) { var hooks = {}; _.each(parts,function (part) { _.chain(part[hook_set_name] || {}) diff --git a/src/templates/admin/plugins-info.html b/src/templates/admin/plugins-info.html new file mode 100644 index 000000000..22f87073e --- /dev/null +++ b/src/templates/admin/plugins-info.html @@ -0,0 +1,30 @@ +<% + var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); +%> + + + + Plugin information + + + + +
+

Etherpad Lite

+
+ +

Installed plugins

+
<%= plugins.formatPlugins() %>
+ +

Installed parts

+
<%= plugins.formatParts() %>
+ +

Installed hooks

+

Server side hooks

+
<%= plugins.formatHooks() %>
+ +

Client side hooks

+
<%= plugins.formatHooks("client_hooks") %>
+
+ + diff --git a/src/templates/admin/plugins.html b/src/templates/admin/plugins.html index 970012696..104f6b986 100644 --- a/src/templates/admin/plugins.html +++ b/src/templates/admin/plugins.html @@ -20,6 +20,9 @@

Etherpad Lite

+ + Technical information on installed plugins +

Installed plugins

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