diff --git a/bin/buildForWindows.sh b/bin/buildForWindows.sh index 6e46e29a1..99f9bb08a 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 2acebd82e..9f691e0af 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 000000000..159c517f9 --- /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 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/settings.json.template_windows b/settings.json.template_windows deleted file mode 100644 index 35b54d8da..000000000 --- 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 6bc777350..ce6d3a00f 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 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/db/ReadOnlyManager.js b/src/node/db/ReadOnlyManager.js index 343406300..b135e6139 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 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 3f6cfa56a..a0aef6648 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 6774b653a..132283a7c 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 028d8ab1b..ffced0476 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 4eb38ea7a..2cfcde82a 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 ba2b462df..1f5336733 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 48750fbcb..002d05aba 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 b1187b097..19180adee 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 1372a313b..742c3bb22 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 1c7bdcfda..86e63f93f 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 19f3f5ff7..1980bdf30 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 87007263f..33953e333 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 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/pad.js b/src/static/js/pad.js index d055a1f2b..df6342e2d 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 51c5a3c6a..af2bc40ad 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("