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