diff --git a/bin/buildForWindows.sh b/bin/buildForWindows.sh index 6e46e29a..99f9bb08 100755 --- a/bin/buildForWindows.sh +++ b/bin/buildForWindows.sh @@ -41,7 +41,7 @@ echo "do a normal unix install first..." bin/installDeps.sh || exit 1 echo "copy the windows settings template..." -cp settings.json.template_windows settings.json +cp settings.json.template settings.json echo "resolve symbolic links..." cp -rL node_modules node_modules_resolved diff --git a/bin/installDeps.sh b/bin/installDeps.sh index 2acebd82..9f691e0a 100755 --- a/bin/installDeps.sh +++ b/bin/installDeps.sh @@ -35,8 +35,9 @@ fi #check node version NODE_VERSION=$(node --version) -if [ ! $(echo $NODE_VERSION | cut -d "." -f 1-2) = "v0.6" ]; then - echo "You're running a wrong version of node, you're using $NODE_VERSION, we need v0.6.x" >&2 +NODE_V_MINOR=$(echo $NODE_VERSION | cut -d "." -f 1-2) +if [ ! $NODE_V_MINOR = "v0.8" ] && [ ! $NODE_V_MINOR = "v0.6" ]; then + echo "You're running a wrong version of node, you're using $NODE_VERSION, we need v0.6.x or v0.8.x" >&2 exit 1 fi diff --git a/bin/installOnWindows.bat b/bin/installOnWindows.bat new file mode 100644 index 00000000..159c517f --- /dev/null +++ b/bin/installOnWindows.bat @@ -0,0 +1,38 @@ +@echo off +set NODE_VERSION=0.8.1 +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 settings.json + +echo _ +echo Installed Etherpad-lite! \ No newline at end of file diff --git a/settings.json.template b/settings.json.template index f89fcd8e..7d175a34 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/settings.json.template_windows b/settings.json.template_windows deleted file mode 100644 index 35b54d8d..00000000 --- a/settings.json.template_windows +++ /dev/null @@ -1,48 +0,0 @@ -/* - This file must be valid JSON. But comments are allowed - - Please edit settings.json, not settings.json.template -*/ -{ - //Ip and port which etherpad should bind at - "ip": "0.0.0.0", - "port" : 9001, - - //The Type of the database. You can choose between sqlite and mysql - "dbType" : "dirty", - //the database specific settings - "dbSettings" : { - "filename" : "var/dirty.db" - }, - - /* An Example of MySQL Configuration - "dbType" : "mysql", - "dbSettings" : { - "user" : "root", - "host" : "localhost", - "password": "", - "database": "store" - }, - */ - - //the default text of a pad - "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", - - /* Users must have a session to access pads. This effectively allows only group pads to be accessed. */ - "requireSession" : false, - - /* Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. */ - "editOnly" : false, - - /* if true, all css & js will be minified before sending to the client. This will improve the loading performance massivly, - but makes it impossible to debug the javascript/css */ - "minify" : false, - - /* This is the path to the Abiword executable. Setting it to null, disables abiword. - Abiword is needed to enable the import/export of pads*/ - "abiword" : null, - - /* cache 6 hours = 1000*60*60*6 */ - "maxAge": 21600000 - -} diff --git a/src/ep.json b/src/ep.json index 6bc77735..ce6d3a00 100644 --- a/src/ep.json +++ b/src/ep.json @@ -1,5 +1,9 @@ { "parts": [ + { "name": "express", "hooks": { + "createServer": "ep_etherpad-lite/node/hooks/express:createServer", + "restartServer": "ep_etherpad-lite/node/hooks/express:restartServer" + } }, { "name": "static", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/static:expressCreateServer" } }, { "name": "specialpages", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/specialpages:expressCreateServer" } }, { "name": "padurlsanitize", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/padurlsanitize:expressCreateServer" } }, diff --git a/src/node/db/API.js b/src/node/db/API.js index 37fd3f16..661b7859 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 f644de12..06b69051 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 b4a39c17..ad2d59f3 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/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js index 34340630..b135e613 100644 --- a/src/node/db/ReadOnlyManager.js +++ b/src/node/db/ReadOnlyManager.js @@ -72,3 +72,33 @@ exports.getPadId = function(readOnlyId, callback) { db.get("readonly2pad:" + 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) { + callback(null, { + readOnlyPadId: value, + padId: padIdOrReadonlyPadId, + readonly: false + }); + }); + } + + if (padIdOrReadonlyPadId.indexOf("r.") != 0) + return handleRealPadId(); + + exports.getPadId(padIdOrReadonlyPadId, function (err, value) { + if(ERR(err, callback)) return; + if (value == null) + return handleRealPadId(); + callback(null, { + readOnlyPadId: padIdOrReadonlyPadId, + padId: value, + readonly: true + }); + }); +} diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index 98b1ed16..40c08441 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 3f6cfa56..a0aef664 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -33,20 +33,20 @@ 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 translates a session to a pad - */ -var session2pad = {}; /** * A associative array that saves which sessions belong to a pad */ var pad2sessions = {}; /** - * A associative array that saves some general informations about a session + * A associative array that saves informations about a session * key = sessionId - * values = author, rev + * values = padId, readonlyPadId, readonly, author, rev + * padId = the real padId of the pad + * readonlyPadId = The readonly pad id of the pad + * readonly = Wether the client has only read access (true) or read/write access (false) * rev = That last revision that was send to this client * author = the author name of this session */ @@ -72,8 +72,7 @@ exports.setSocketIO = function(socket_io) */ exports.handleConnect = function(client) { - //Initalize session2pad and sessioninfos for this new session - session2pad[client.id]=null; + //Initalize sessioninfos for this new session sessioninfos[client.id]={}; } @@ -101,7 +100,7 @@ exports.kickSessionsFromPad = function(padID) exports.handleDisconnect = function(client) { //save the padname of this session - var sessionPad=session2pad[client.id]; + var sessionPad=sessioninfos[client.id].padId; //if this connection was already etablished with a handshake, send a disconnect message to the others if(sessioninfos[client.id] && sessioninfos[client.id].author) @@ -149,8 +148,7 @@ exports.handleDisconnect = function(client) } } - //Delete the session2pad and sessioninfos entrys of this session - delete session2pad[client.id]; + //Delete the sessioninfos entrys of this session delete sessioninfos[client.id]; } @@ -161,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!"); @@ -171,46 +174,76 @@ 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 == "COLLABROOM" && typeof message.data == 'object'){ - if (message.data.type == "USER_CHANGES") - { - handleUserChanges(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 Message Type " + message.type); } - 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 == "CLIENT_MESSAGE" && - typeof message.data.payload == 'object' && - message.data.payload.type == "suggestUserName") - { - handleSuggestUserName(client, message); - } - } - //if the message type is unknown, throw an exception - else - { - 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 { + finalHandler(); } } + /** * Handles a save revision message * @param client the client that send this message * @param message the message from the client */ function handleSaveRevisionMessage(client, message){ - var padId = session2pad[client.id]; + var padId = sessioninfos[client.id].padId; var userId = sessioninfos[client.id].author; padManager.getPad(padId, function(err, pad) @@ -231,7 +264,7 @@ function handleChatMessage(client, message) var time = new Date().getTime(); var userId = sessioninfos[client.id].author; var text = message.data.text; - var padId = session2pad[client.id]; + var padId = sessioninfos[client.id].padId; var pad; var userName; @@ -307,7 +340,7 @@ function handleSuggestUserName(client, message) return; } - var padId = session2pad[client.id]; + var padId = sessioninfos[client.id].padId; //search the author and send him this message for(var i in pad2sessions[padId]) @@ -341,7 +374,7 @@ function handleUserInfoUpdate(client, message) authorManager.setAuthorColorId(author, message.data.userInfo.colorId); authorManager.setAuthorName(author, message.data.userInfo.name); - var padId = session2pad[client.id]; + var padId = sessioninfos[client.id].padId; //set a null name, when there is no name set. cause the client wants it null if(message.data.userInfo.name == null) @@ -387,7 +420,7 @@ function handleUserChanges(client, message) messageLogger.warn("Dropped message, USER_CHANGES Message has no changeset!"); return; } - + //get all Vars we need var baseRev = message.data.baseRev; var wireApool = (new AttributePool()).fromJsonable(message.data.apool); @@ -399,7 +432,7 @@ function handleUserChanges(client, message) //get the pad function(callback) { - padManager.getPad(session2pad[client.id], function(err, value) + padManager.getPad(sessioninfos[client.id].padId, function(err, value) { if(ERR(err, callback)) return; pad = value; @@ -506,17 +539,15 @@ exports.updatePadClients = function(pad, callback) //go trough all sessions on this pad async.forEach(pad2sessions[pad.id], function(session, callback) { - var lastRev = sessioninfos[session].rev; - + //https://github.com/caolan/async#whilst //send them all new changesets async.whilst( - function (){ return lastRev < pad.getHeadRevisionNumber()}, + function (){ return sessioninfos[session].rev < pad.getHeadRevisionNumber()}, function(callback) - { - var author, revChangeset; - - var r = ++lastRev; + { + var author, revChangeset, currentTime; + var r = sessioninfos[session].rev + 1; async.parallel([ function (callback) @@ -536,6 +567,15 @@ exports.updatePadClients = function(pad, callback) revChangeset = value; callback(); }); + }, + function (callback) + { + pad.getRevisionDate(r, function(err, date) + { + if(ERR(err, callback)) return; + currentTime = date; + callback(); + }); } ], function(err) { @@ -553,24 +593,30 @@ exports.updatePadClients = function(pad, callback) else { var forWire = Changeset.prepareForWire(revChangeset, pad.pool); - var wireMsg = {"type":"COLLABROOM","data":{type:"NEW_CHANGES", newRev:r, - changeset: forWire.translated, - apool: forWire.pool, - author: author}}; + var wireMsg = {"type":"COLLABROOM", + "data":{type:"NEW_CHANGES", + newRev:r, + changeset: forWire.translated, + apool: forWire.pool, + author: author, + currentTime: currentTime, + timeDelta: currentTime - sessioninfos[session].time + }}; socketio.sockets.sockets[session].json.send(wireMsg); } + + if(sessioninfos[session] != null) + { + sessioninfos[session].time = currentTime; + sessioninfos[session].rev = r; + } callback(null); }); }, callback ); - - if(sessioninfos[session] != null) - { - sessioninfos[session].rev = pad.getHeadRevisionNumber(); - } },callback); } @@ -655,14 +701,29 @@ function handleClientReady(client, message) var authorColorId; var pad; var historicalAuthorData = {}; - var readOnlyId; + var currentTime; var chatMessages; + var padIds; async.series([ + // Get ro/rw id:s + function (callback) { + readOnlyManager.getIds(message.padId, function(err, value) { + if(ERR(err, callback)) return; + padIds = value; + callback(); + }); + }, + //check permissions function(callback) { - securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, function(err, statusObject) + // Note: message.sessionID is an entierly 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 (padIds.padId, message.sessionID, message.token, message.password, function(err, statusObject) { if(ERR(err, callback)) return; @@ -705,21 +766,12 @@ function handleClientReady(client, message) }, function(callback) { - padManager.getPad(message.padId, function(err, value) + padManager.getPad(padIds.padId, function(err, value) { if(ERR(err, callback)) return; pad = value; callback(); }); - }, - function(callback) - { - readOnlyManager.getReadOnlyId(message.padId, function(err, value) - { - if(ERR(err, callback)) return; - readOnlyId = value; - callback(); - }); } ], callback); }, @@ -729,6 +781,16 @@ function handleClientReady(client, message) var authors = pad.getAllAuthors(); async.parallel([ + //get timestamp of latest revission needed for timeslider + function(callback) + { + pad.getRevisionDate(pad.getHeadRevisionNumber(), function(err, date) + { + if(ERR(err, callback)) return; + currentTime = date; + callback(); + }); + }, //get all author data out of the database function(callback) { @@ -755,35 +817,36 @@ function handleClientReady(client, message) } ], callback); - }, function(callback) { //Check if this author is already on the pad, if yes, kick the other sessions! - if(pad2sessions[message.padId]) + if(pad2sessions[padIds.padId]) { - for(var i in pad2sessions[message.padId]) + for(var i in pad2sessions[padIds.padId]) { - if(sessioninfos[pad2sessions[message.padId][i]] && sessioninfos[pad2sessions[message.padId][i]].author == author) + if(sessioninfos[pad2sessions[padIds.padId][i]] && sessioninfos[pad2sessions[padIds.padId][i]].author == author) { - var socket = socketio.sockets.sockets[pad2sessions[message.padId][i]]; + var socket = socketio.sockets.sockets[pad2sessions[padIds.padId][i]]; if(socket) socket.json.send({disconnect:"userdup"}); } } } - //Save in session2pad that this session belonges to this pad + //Save in sessioninfos that this session belonges to this pad var sessionId=String(client.id); - session2pad[sessionId] = message.padId; + sessioninfos[sessionId].padId = padIds.padId; + sessioninfos[sessionId].readOnlyPadId = padIds.readOnlyPadId; + sessioninfos[sessionId].readonly = padIds.readonly; //check if there is already a pad2sessions entry, if not, create one - if(!pad2sessions[message.padId]) + if(!pad2sessions[padIds.padId]) { - pad2sessions[message.padId] = []; + pad2sessions[padIds.padId] = []; } //Saves in pad2sessions that this session belongs to this pad - pad2sessions[message.padId].push(sessionId); + pad2sessions[padIds.padId].push(sessionId); //prepare all values for the wire var atext = Changeset.cloneAText(pad.atext); @@ -791,6 +854,9 @@ function handleClientReady(client, message) var apool = attribsForWire.pool.toJsonable(); atext.attribs = attribsForWire.translated; + // Warning: never ever send padIds.padId to the client. If the + // client is read only you would open a security hole 1 swedish + // mile wide... var clientVars = { "accountPrivs": { "maxRevisions": 100 @@ -799,6 +865,7 @@ function handleClientReady(client, message) "initialOptions": { "guestPolicy": "deny" }, + "savedRevisions": pad.getSavedRevisions(), "collab_client_vars": { "initialAttributedText": atext, "clientIp": "127.0.0.1", @@ -807,7 +874,8 @@ function handleClientReady(client, message) "historicalAuthorData": historicalAuthorData, "apool": apool, "rev": pad.getHeadRevisionNumber(), - "globalPadId": message.padId + "globalPadId": message.padId, + "time": currentTime, }, "colorPalette": ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd", "#4c9c82", "#12d1ad", "#2d8e80", "#7485c3", "#a091c7", "#3185ab", "#6818b4", "#e6e76d", "#a42c64", "#f386e5", "#4ecc0c", "#c0c236", "#693224", "#b5de6a", "#9b88fd", "#358f9b", "#496d2f", "#e267fe", "#d23056", "#1a1a64", "#5aa335", "#d722bb", "#86dc6c", "#b5a714", "#955b6a", "#9f2985", "#4b81c8", "#3d6a5b", "#434e16", "#d16084", "#af6a0e", "#8c8bd8"], "clientIp": "127.0.0.1", @@ -817,9 +885,10 @@ function handleClientReady(client, message) "initialTitle": "Pad: " + message.padId, "opts": {}, "chatHistory": chatMessages, - "numConnectedUsers": pad2sessions[message.padId].length, + "numConnectedUsers": pad2sessions[padIds.padId].length, "isProPad": false, - "readOnlyId": readOnlyId, + "readOnlyId": padIds.readOnlyPadId, + "readonly": padIds.readonly, "serverTimestamp": new Date().getTime(), "globalPadId": message.padId, "userId": author, @@ -831,7 +900,9 @@ function handleClientReady(client, message) "plugins": { "plugins": plugins.plugins, "parts": plugins.parts, - } + }, + "initialChangesets": [] // FIXME: REMOVE THIS SHIT + } //Add a username to the clientVars if one avaiable @@ -852,7 +923,7 @@ function handleClientReady(client, message) else { //Send the clientVars to the Client - client.json.send(clientVars); + client.json.send({type: "CLIENT_VARS", data: clientVars}); //Save the revision in sessioninfos sessioninfos[client.id].rev = pad.getHeadRevisionNumber(); } @@ -882,7 +953,7 @@ function handleClientReady(client, message) } //Run trough all sessions of this pad - async.forEach(pad2sessions[message.padId], function(sessionID, callback) + async.forEach(pad2sessions[padIds.padId], function(sessionID, callback) { var author, socket, sessionAuthorName, sessionAuthorColorId; @@ -955,3 +1026,347 @@ function handleClientReady(client, message) ERR(err); }); } + +/** + * Handles a request for a rough changeset, the timeslider client needs it + */ +function handleChangesetRequest(client, message) +{ + //check if all ok + if(message.data == null) + { + messageLogger.warn("Dropped message, changeset request has no data!"); + return; + } + if(message.padId == null) + { + messageLogger.warn("Dropped message, changeset request has no padId!"); + return; + } + if(message.data.granularity == null) + { + messageLogger.warn("Dropped message, changeset request has no granularity!"); + return; + } + if(message.data.start == null) + { + messageLogger.warn("Dropped message, changeset request has no start!"); + return; + } + if(message.data.requestID == null) + { + messageLogger.warn("Dropped message, changeset request has no requestID!"); + return; + } + + var granularity = message.data.granularity; + var start = message.data.start; + var end = start + (100 * granularity); + var padIds; + + async.series([ + function (callback) { + readOnlyManager.getIds(message.padId, function(err, value) { + if(ERR(err, callback)) return; + padIds = value; + callback(); + }); + }, + function (callback) { + //build the requested rough changesets and send them back + getChangesetInfo(padIds.padId, start, end, granularity, function(err, changesetInfo) + { + ERR(err); + + var data = changesetInfo; + data.requestID = message.data.requestID; + + client.json.send({type: "CHANGESET_REQ", data: data}); + }); + } + ]); +} + + +/** + * Tries to rebuild the getChangestInfo function of the original Etherpad + * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 + */ +function getChangesetInfo(padId, startNum, endNum, granularity, callback) +{ + var forwardsChangesets = []; + var backwardsChangesets = []; + var timeDeltas = []; + var apool = new AttributePool(); + var pad; + var composedChangesets = {}; + var revisionDate = []; + var lines; + + async.series([ + //get the pad from the database + function(callback) + { + padManager.getPad(padId, function(err, _pad) + { + if(ERR(err, callback)) return; + pad = _pad; + callback(); + }); + }, + function(callback) + { + //calculate the last full endnum + var lastRev = pad.getHeadRevisionNumber(); + if (endNum > lastRev+1) { + endNum = lastRev+1; + } + endNum = Math.floor(endNum / granularity)*granularity; + + var compositesChangesetNeeded = []; + var revTimesNeeded = []; + + //figure out which composite Changeset and revTimes we need, to load them in bulk + var compositeStart = startNum; + while (compositeStart < endNum) + { + var compositeEnd = compositeStart + granularity; + + //add the composite Changeset we needed + compositesChangesetNeeded.push({start: compositeStart, end: compositeEnd}); + + //add the t1 time we need + revTimesNeeded.push(compositeStart == 0 ? 0 : compositeStart - 1); + //add the t2 time we need + revTimesNeeded.push(compositeEnd - 1); + + compositeStart += granularity; + } + + //get all needed db values parallel + async.parallel([ + function(callback) + { + //get all needed composite Changesets + async.forEach(compositesChangesetNeeded, function(item, callback) + { + composePadChangesets(padId, item.start, item.end, function(err, changeset) + { + if(ERR(err, callback)) return; + composedChangesets[item.start + "/" + item.end] = changeset; + callback(); + }); + }, callback); + }, + function(callback) + { + //get all needed revision Dates + async.forEach(revTimesNeeded, function(revNum, callback) + { + pad.getRevisionDate(revNum, function(err, revDate) + { + if(ERR(err, callback)) return; + revisionDate[revNum] = Math.floor(revDate/1000); + callback(); + }); + }, callback); + }, + //get the lines + function(callback) + { + getPadLines(padId, startNum-1, function(err, _lines) + { + if(ERR(err, callback)) return; + lines = _lines; + callback(); + }); + } + ], callback); + }, + //doesn't know what happens here excatly :/ + function(callback) + { + var compositeStart = startNum; + + while (compositeStart < endNum) + { + if (compositeStart + granularity > endNum) + { + break; + } + + var compositeEnd = compositeStart + granularity; + + var forwards = composedChangesets[compositeStart + "/" + compositeEnd]; + var backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); + + Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool()); + Changeset.mutateTextLines(forwards, lines.textlines); + + var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); + var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); + + var t1, t2; + if (compositeStart == 0) + { + t1 = revisionDate[0]; + } + else + { + t1 = revisionDate[compositeStart - 1]; + } + + t2 = revisionDate[compositeEnd - 1]; + + timeDeltas.push(t2 - t1); + forwardsChangesets.push(forwards2); + backwardsChangesets.push(backwards2); + + compositeStart += granularity; + } + + callback(); + } + ], function(err) + { + if(ERR(err, callback)) return; + + callback(null, {forwardsChangesets: forwardsChangesets, + backwardsChangesets: backwardsChangesets, + apool: apool.toJsonable(), + actualEndNum: endNum, + timeDeltas: timeDeltas, + start: startNum, + granularity: granularity }); + }); +} + +/** + * Tries to rebuild the getPadLines function of the original Etherpad + * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263 + */ +function getPadLines(padId, revNum, callback) +{ + var atext; + var result = {}; + var pad; + + async.series([ + //get the pad from the database + function(callback) + { + padManager.getPad(padId, function(err, _pad) + { + if(ERR(err, callback)) return; + pad = _pad; + callback(); + }); + }, + //get the atext + function(callback) + { + if(revNum >= 0) + { + pad.getInternalRevisionAText(revNum, function(err, _atext) + { + if(ERR(err, callback)) return; + atext = _atext; + callback(); + }); + } + else + { + atext = Changeset.makeAText("\n"); + callback(null); + } + }, + function(callback) + { + result.textlines = Changeset.splitTextLines(atext.text); + result.alines = Changeset.splitAttributionLines(atext.attribs, atext.text); + callback(null); + } + ], function(err) + { + if(ERR(err, callback)) return; + callback(null, result); + }); +} + +/** + * Tries to rebuild the composePadChangeset function of the original Etherpad + * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241 + */ +function composePadChangesets(padId, startNum, endNum, callback) +{ + var pad; + var changesets = []; + var changeset; + + async.series([ + //get the pad from the database + function(callback) + { + padManager.getPad(padId, function(err, _pad) + { + if(ERR(err, callback)) return; + pad = _pad; + callback(); + }); + }, + //fetch all changesets we need + function(callback) + { + var changesetsNeeded=[]; + + //create a array for all changesets, we will + //replace the values with the changeset later + for(var r=startNum;r lastRev+1) { - endNum = lastRev+1; - } - endNum = Math.floor(endNum / granularity)*granularity; - - var compositesChangesetNeeded = []; - var revTimesNeeded = []; - - //figure out which composite Changeset and revTimes we need, to load them in bulk - var compositeStart = startNum; - while (compositeStart < endNum) - { - var compositeEnd = compositeStart + granularity; - - //add the composite Changeset we needed - compositesChangesetNeeded.push({start: compositeStart, end: compositeEnd}); - - //add the t1 time we need - revTimesNeeded.push(compositeStart == 0 ? 0 : compositeStart - 1); - //add the t2 time we need - revTimesNeeded.push(compositeEnd - 1); - - compositeStart += granularity; - } - - //get all needed db values parallel - async.parallel([ - function(callback) - { - //get all needed composite Changesets - async.forEach(compositesChangesetNeeded, function(item, callback) - { - composePadChangesets(padId, item.start, item.end, function(err, changeset) - { - if(ERR(err, callback)) return; - composedChangesets[item.start + "/" + item.end] = changeset; - callback(); - }); - }, callback); - }, - function(callback) - { - //get all needed revision Dates - async.forEach(revTimesNeeded, function(revNum, callback) - { - pad.getRevisionDate(revNum, function(err, revDate) - { - if(ERR(err, callback)) return; - revisionDate[revNum] = Math.floor(revDate/1000); - callback(); - }); - }, callback); - }, - //get the lines - function(callback) - { - getPadLines(padId, startNum-1, function(err, _lines) - { - if(ERR(err, callback)) return; - lines = _lines; - callback(); - }); - } - ], callback); - }, - //doesn't know what happens here excatly :/ - function(callback) - { - var compositeStart = startNum; - - while (compositeStart < endNum) - { - if (compositeStart + granularity > endNum) - { - break; - } - - var compositeEnd = compositeStart + granularity; - - var forwards = composedChangesets[compositeStart + "/" + compositeEnd]; - var backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); - - Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool()); - Changeset.mutateTextLines(forwards, lines.textlines); - - var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); - var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); - - var t1, t2; - if (compositeStart == 0) - { - t1 = revisionDate[0]; - } - else - { - t1 = revisionDate[compositeStart - 1]; - } - - t2 = revisionDate[compositeEnd - 1]; - - timeDeltas.push(t2 - t1); - forwardsChangesets.push(forwards2); - backwardsChangesets.push(backwards2); - - compositeStart += granularity; - } - - callback(); - } - ], function(err) - { - if(ERR(err, callback)) return; - - callback(null, {forwardsChangesets: forwardsChangesets, - backwardsChangesets: backwardsChangesets, - apool: apool.toJsonable(), - actualEndNum: endNum, - timeDeltas: timeDeltas, - start: startNum, - granularity: granularity }); - }); -} - -/** - * Tries to rebuild the getPadLines function of the original Etherpad - * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263 - */ -function getPadLines(padId, revNum, callback) -{ - var atext; - var result = {}; - var pad; - - async.series([ - //get the pad from the database - function(callback) - { - padManager.getPad(padId, function(err, _pad) - { - if(ERR(err, callback)) return; - pad = _pad; - callback(); - }); - }, - //get the atext - function(callback) - { - if(revNum >= 0) - { - pad.getInternalRevisionAText(revNum, function(err, _atext) - { - if(ERR(err, callback)) return; - atext = _atext; - callback(); - }); - } - else - { - atext = Changeset.makeAText("\n"); - callback(null); - } - }, - function(callback) - { - result.textlines = Changeset.splitTextLines(atext.text); - result.alines = Changeset.splitAttributionLines(atext.attribs, atext.text); - callback(null); - } - ], function(err) - { - if(ERR(err, callback)) return; - callback(null, result); - }); -} - -/** - * Tries to rebuild the composePadChangeset function of the original Etherpad - * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241 - */ -function composePadChangesets(padId, startNum, endNum, callback) -{ - var pad; - var changesets = []; - var changeset; - - async.series([ - //get the pad from the database - function(callback) - { - padManager.getPad(padId, function(err, _pad) - { - if(ERR(err, callback)) return; - pad = _pad; - callback(); - }); - }, - //fetch all changesets we need - function(callback) - { - var changesetsNeeded=[]; - - //create a array for all changesets, we will - //replace the values with the changeset later - for(var r=startNum;r' + real_url + '', 302); + 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/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index 6774b653..132283a7 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -5,7 +5,6 @@ var socketIORouter = require("../../handler/SocketIORouter"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var padMessageHandler = require("../../handler/PadMessageHandler"); -var timesliderMessageHandler = require("../../handler/TimesliderMessageHandler"); var connect = require('connect'); @@ -59,7 +58,6 @@ exports.expressCreateServer = function (hook_name, args, cb) { //Initalize the Socket.IO Router socketIORouter.setSocketIO(io); socketIORouter.addComponent("pad", padMessageHandler); - socketIORouter.addComponent("timeslider", timesliderMessageHandler); hooks.callAll("socketio", {"app": args.app, "io": io}); } diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 028d8ab1..ffced047 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -88,6 +88,8 @@ exports.basicAuth = function (req, res, next) { }); } +var secret = null; + exports.expressConfigure = function (hook_name, args, cb) { // 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. @@ -100,10 +102,15 @@ exports.expressConfigure = function (hook_name, args, cb) { * name) to a javascript identifier compatible string. Makes code * handling it cleaner :) */ - args.app.sessionStore = new express.session.MemoryStore(); + if (!exports.sessionStore) { + exports.sessionStore = new express.session.MemoryStore(); + secret = randomString(32); + } + + args.app.sessionStore = exports.sessionStore; args.app.use(express.session({store: args.app.sessionStore, key: 'express_sid', - secret: apikey = randomString(32)})); + secret: secret})); args.app.use(exports.basicAuth); } diff --git a/src/node/server.js b/src/node/server.js index 4eb38ea7..2cfcde82 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -22,36 +22,13 @@ */ var log4js = require('log4js'); -var fs = require('fs'); var settings = require('./utils/Settings'); var db = require('./db/DB'); var async = require('async'); -var express = require('express'); -var path = require('path'); var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var npm = require("npm/lib/npm.js"); -var _ = require("underscore"); -//try to get the git version -var version = ""; -try -{ - var rootPath = path.resolve(npm.dir, '..'); - var ref = fs.readFileSync(rootPath + "/.git/HEAD", "utf-8"); - 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); -} -catch(e) -{ - console.warn("Can't get git version for server header\n" + e.message) -} - -console.log("Report bugs at https://github.com/Pita/etherpad-lite/issues") - -var serverName = "Etherpad-Lite " + version + " (http://j.mp/ep-lite)"; //set loglevel log4js.setGlobalLogLevel(settings.loglevel); @@ -75,27 +52,7 @@ async.waterfall([ //initalize the http server function (callback) { - //create server - var app = express.createServer(); - - app.use(function (req, res, next) { - res.header("Server", serverName); - next(); - }); - - app.configure(function() { hooks.callAll("expressConfigure", {"app": app}); }); - - hooks.callAll("expressCreateServer", {"app": app}); - - //let the server listen - app.listen(settings.port, settings.ip); - console.log("You can access your Etherpad-Lite 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"); - } - else{ - console.warn("Admin username and password not set in settings.json. To access admin please uncomment and edit 'users' in settings.json"); - } + hooks.callAll("createServer", {}); callback(null); } ]); diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js index ba2b462d..1f533673 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.js @@ -21,9 +21,12 @@ var path = require('path'); var zlib = require('zlib'); 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 CACHE_DIR = path.normalize(path.join(settings.root, 'var/')); -CACHE_DIR = path.existsSync(CACHE_DIR) ? CACHE_DIR : undefined; +CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined; var responseCache = {}; diff --git a/src/package.json b/src/package.json index 48750fbc..002d05ab 100644 --- a/src/package.json +++ b/src/package.json @@ -16,9 +16,9 @@ "resolve" : "0.2.1", "socket.io" : "0.9.6", "ueberDB" : "0.1.7", - "async" : "0.1.18", - "express" : "2.5.8", - "connect" : "1.8.7", + "async" : "0.1.x", + "express" : "2.5.x", + "connect" : "1.x", "clean-css" : "0.3.2", "uglify-js" : "1.2.5", "formidable" : "1.0.9", diff --git a/src/static/css/pad.css b/src/static/css/pad.css index b1187b09..19180ade 100644 --- a/src/static/css/pad.css +++ b/src/static/css/pad.css @@ -19,6 +19,9 @@ textarea { iframe { position: absolute } +.readonly .acl-write { + display: none; +} #users { background: #f7f7f7; background: -webkit-linear-gradient( #F7F7F7,#EEE); @@ -992,4 +995,7 @@ input[type=checkbox] { .toolbar ul li .separator { display: none } + #online_count { + line-height: 24px + } } diff --git a/src/static/js/admin/plugins.js b/src/static/js/admin/plugins.js index 1372a313..742c3bb2 100644 --- a/src/static/js/admin/plugins.js +++ b/src/static/js/admin/plugins.js @@ -126,6 +126,7 @@ $(document).ready(function () { socket.on('installed-results', function (data) { $("#installed-plugins *").remove(); for (plugin_name in data.results) { + if (plugin_name == "ep_etherpad-lite") continue; // Hack... var plugin = data.results[plugin_name]; var row = $("#installed-plugin-template").clone(); diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js index 1c7bdcfd..86e63f93 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -84,14 +84,14 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro var appLevelDisconnectReason = null; var padContents = { - currentRevision: clientVars.revNum, - currentTime: clientVars.currentTime, - currentLines: Changeset.splitTextLines(clientVars.initialStyledContents.atext.text), + currentRevision: clientVars.collab_client_vars.rev, + currentTime: clientVars.collab_client_vars.time, + currentLines: Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text), currentDivs: null, // to be filled in once the dom loads - apool: (new AttribPool()).fromJsonable(clientVars.initialStyledContents.apool), + apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool), alines: Changeset.splitAttributionLines( - clientVars.initialStyledContents.atext.attribs, clientVars.initialStyledContents.atext.text), + clientVars.collab_client_vars.initialAttributedText.attribs, clientVars.collab_client_vars.initialAttributedText.text), // generates a jquery element containing HTML for a line lineToElement: function(line, aline) @@ -271,7 +271,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro Changeset.mutateTextLines(changeset, padContents); padContents.currentRevision = revision; - padContents.currentTime += timeDelta * 1000; + padContents.currentTime += timeDelta; debugLog('Time Delta: ', timeDelta) updateTimer(); @@ -432,19 +432,6 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro var start = request.rev; var requestID = Math.floor(Math.random() * 100000); -/*var msg = { "component" : "timeslider", - "type":"CHANGESET_REQ", - "padId": padId, - "token": token, - "protocolVersion": 2, - "data" - { - "start": start, - "granularity": granularity - }}; - - socket.send(msg);*/ - sendSocketMsg("CHANGESET_REQ", { "start": start, "granularity": granularity, @@ -452,19 +439,6 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro }); self.reqCallbacks[requestID] = callback; - -/*debugLog("loadinging revision", start, "through ajax"); - $.getJSON("/ep/pad/changes/" + clientVars.padIdForUrl + "?s=" + start + "&g=" + granularity, function (data, textStatus) - { - if (textStatus !== "success") - { - console.log(textStatus); - BroadcastSlider.showReconnectUI(); - } - self.handleResponse(data, start, granularity, callback); - - setTimeout(self.loadFromQueue, 10); // load the next ajax function - });*/ }, handleSocketResponse: function(message) { @@ -493,130 +467,58 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]); } if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1); + }, + handleMessageFromServer: function (obj) + { + debugLog("handleMessage:", arguments); + + if (obj.type == "COLLABROOM") + { + obj = obj.data; + + if (obj.type == "NEW_CHANGES") + { + debugLog(obj); + var changeset = Changeset.moveOpsToNewPool( + obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); + + var changesetBack = Changeset.inverse( + obj.changeset, padContents.currentLines, padContents.alines, padContents.apool); + + var changesetBack = Changeset.moveOpsToNewPool( + changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); + + loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta); + } + else if (obj.type == "NEW_AUTHORDATA") + { + var authorMap = {}; + authorMap[obj.author] = obj.data; + receiveAuthorData(authorMap); + + var authors = _.map(padContents.getActiveAuthors(), function(name) { + return authorData[name]; + }); + + BroadcastSlider.setAuthors(authors); + } + else if (obj.type == "NEW_SAVEDREV") + { + var savedRev = obj.savedRev; + BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev); + } + } + else if(obj.type == "CHANGESET_REQ") + { + changesetLoader.handleSocketResponse(obj); + } + else + { + debugLog("Unknown message type: " + obj.type); + } } }; - function handleMessageFromServer() - { - debugLog("handleMessage:", arguments); - var obj = arguments[0]['data']; - var expectedType = "COLLABROOM"; - - obj = JSON.parse(obj); - if (obj['type'] == expectedType) - { - obj = obj['data']; - - if (obj['type'] == "NEW_CHANGES") - { - debugLog(obj); - var changeset = Changeset.moveOpsToNewPool( - obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); - - var changesetBack = Changeset.moveOpsToNewPool( - obj.changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); - - loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta); - } - else if (obj['type'] == "NEW_AUTHORDATA") - { - var authorMap = {}; - authorMap[obj.author] = obj.data; - receiveAuthorData(authorMap); - - var authors = _.map(padContents.getActiveAuthors(), function(name) { - return authorData[name]; - }); - - BroadcastSlider.setAuthors(authors); - } - else if (obj['type'] == "NEW_SAVEDREV") - { - var savedRev = obj.savedRev; - BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev); - } - } - else - { - debugLog("incorrect message type: " + obj['type'] + ", expected " + expectedType); - } - } - - function handleSocketClosed(params) - { - debugLog("socket closed!", params); - socket = null; - - BroadcastSlider.showReconnectUI(); - // var reason = appLevelDisconnectReason || params.reason; - // var shouldReconnect = params.reconnect; - // if (shouldReconnect) { - // // determine if this is a tight reconnect loop due to weird connectivity problems - // // reconnectTimes.push(+new Date()); - // var TOO_MANY_RECONNECTS = 8; - // var TOO_SHORT_A_TIME_MS = 10000; - // if (reconnectTimes.length >= TOO_MANY_RECONNECTS && - // ((+new Date()) - reconnectTimes[reconnectTimes.length-TOO_MANY_RECONNECTS]) < - // TOO_SHORT_A_TIME_MS) { - // setChannelState("DISCONNECTED", "looping"); - // } - // else { - // setChannelState("RECONNECTING", reason); - // setUpSocket(); - // } - // } - // else { - // BroadcastSlider.showReconnectUI(); - // setChannelState("DISCONNECTED", reason); - // } - } - - function sendMessage(msg) - { - socket.postMessage(JSON.stringify( - { - type: "COLLABROOM", - data: msg - })); - } - - - function setChannelState(newChannelState, moreInfo) - { - if (newChannelState != channelState) - { - channelState = newChannelState; - // callbacks.onChannelStateChange(channelState, moreInfo); - } - } - - function abandonConnection(reason) - { - if (socket) - { - socket.onclosed = function() - {}; - socket.onhiccup = function() - {}; - socket.disconnect(); - } - socket = null; - setChannelState("DISCONNECTED", reason); - } - - /// Since its not used, import 'forEach' has been dropped -/*window['onloadFuncts'] = []; - window.onload = function () - { - window['isloaded'] = true; - - - forEach(window['onloadFuncts'],function (funct) - { - funct(); - }); - };*/ - // to start upon window load, just push a function onto this array //window['onloadFuncts'].push(setUpSocket); //window['onloadFuncts'].push(function () @@ -637,36 +539,19 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro // this is necessary to keep infinite loops of events firing, // since goToRevision changes the slider position var goToRevisionIfEnabledCount = 0; - var goToRevisionIfEnabled = function() + var goToRevisionIfEnabled = function() { + if (goToRevisionIfEnabledCount > 0) { - if (goToRevisionIfEnabledCount > 0) - { - goToRevisionIfEnabledCount--; - } - else - { - goToRevision.apply(goToRevision, arguments); - } - } - - - - + goToRevisionIfEnabledCount--; + } + else + { + goToRevision.apply(goToRevision, arguments); + } + } BroadcastSlider.onSlider(goToRevisionIfEnabled); - (function() - { - for (var i = 0; i < clientVars.initialChangesets.length; i++) - { - var csgroup = clientVars.initialChangesets[i]; - var start = clientVars.initialChangesets[i].start; - var granularity = clientVars.initialChangesets[i].granularity; - debugLog("loading changest on startup: ", start, granularity, csgroup); - changesetLoader.handleResponse(csgroup, start, granularity, null); - } - })(); - var dynamicCSS = makeCSSManager('dynamicsyntax'); var authorData = {}; @@ -686,7 +571,7 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro } } - receiveAuthorData(clientVars.historicalAuthorData); + receiveAuthorData(clientVars.collab_client_vars.historicalAuthorData); return changesetLoader; } diff --git a/src/static/js/broadcast_revisions.js b/src/static/js/broadcast_revisions.js index 19f3f5ff..1980bdf3 100644 --- a/src/static/js/broadcast_revisions.js +++ b/src/static/js/broadcast_revisions.js @@ -57,7 +57,7 @@ function loadBroadcastRevisionsJS() endRevision.addChangeset(fromIndex, backChangeset, -1 * timeDelta); } - revisionInfo.latest = clientVars.totalRevs || -1; + revisionInfo.latest = clientVars.collab_client_vars.rev || -1; revisionInfo.createNew = function(index) { diff --git a/src/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.js index 87007263..33953e33 100644 --- a/src/static/js/broadcast_slider.js +++ b/src/static/js/broadcast_slider.js @@ -23,6 +23,7 @@ // These parameters were global, now they are injected. A reference to the // Timeslider controller would probably be more appropriate. var _ = require('./underscore'); +var padmodals = require('./pad_modals').padmodals; function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) { @@ -54,11 +55,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) { slidercallbacks[i](newval); } - } - - - - + } var updateSliderElements = function() { @@ -68,12 +65,8 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) savedRevisions[i].css('left', (position * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)) - 1); } $("#ui-slider-handle").css('left', sliderPos * ($("#ui-slider-bar").width() - 2) / (sliderLength * 1.0)); - } - - - - - + } + var addSavedRevision = function(position, info) { var newSavedRevision = $('
'); @@ -88,7 +81,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) BroadcastSlider.setSliderPosition(position); }); savedRevisions.push(newSavedRevision); - }; + }; var removeSavedRevision = function(position) { @@ -96,7 +89,7 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) savedRevisions.remove(element); element.remove(); return element; - }; + }; /* Begin small 'API' */ @@ -162,9 +155,9 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) function showReconnectUI() { - $("#padmain, #rightbars").css('top', "130px"); - $("#timeslider").show(); - $('#error').show(); + var cls = 'modaldialog cboxdisconnected cboxdisconnected_unknown'; + $("#connectionbox").get(0).className = cls; + padmodals.showModal("#connectionbox", 500); } var fixPadHeight = _.throttle(function(){ @@ -481,8 +474,8 @@ function loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded) } $("#timeslider").show(); - setSliderLength(clientVars.totalRevs); - setSliderPosition(clientVars.revNum); + setSliderLength(clientVars.collab_client_vars.rev); + setSliderPosition(clientVars.collab_client_vars.rev); _.each(clientVars.savedRevisions, function(revision) { diff --git a/src/static/js/chat.js b/src/static/js/chat.js index 23b47667..47b0ae3c 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/pad.js b/src/static/js/pad.js index d055a1f2..df6342e2 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -310,14 +310,20 @@ function handshake() receivedClientVars = true; //set some client vars - clientVars = obj; + clientVars = obj.data; clientVars.userAgent = "Anonymous"; clientVars.collab_client_vars.clientAgent = "Anonymous"; - + //initalize the pad pad._afterHandshake(); initalized = true; + $("body").addClass(clientVars.readonly ? "readonly" : "readwrite") + + padeditor.ace.callWithAce(function (ace) { + ace.ace_setEditable(!clientVars.readonly); + }); + // If the LineNumbersDisabled value is set to true then we need to hide the Line Numbers if (settings.LineNumbersDisabled == true) { @@ -354,6 +360,8 @@ function handshake() //this message advices the client to disconnect if (obj.disconnect) { + console.warn("FORCED TO DISCONNECT"); + console.warn(obj); padconnectionstatus.disconnected(obj.disconnect); socket.disconnect(); return; diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 51c5a3c6..af2bc40a 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -241,7 +241,7 @@ var padeditbar = (function() if ($('#readonlyinput').is(':checked')) { var basePath = document.location.href.substring(0, document.location.href.indexOf("/p/")); - var readonlyLink = basePath + "/ro/" + clientVars.readOnlyId; + var readonlyLink = basePath + "/p/" + clientVars.readOnlyId; $('#embedinput').val("