From 914d79ad20d61920996ec35326c199f7e38a0050 Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Mon, 23 Apr 2012 12:52:30 +0200 Subject: [PATCH 01/23] Unified timeslider and pad editing protocol / component --- src/node/handler/PadMessageHandler.js | 348 +++++++++++- src/node/handler/TimesliderMessageHandler.js | 534 ------------------- src/node/hooks/express/socketio.js | 2 - src/static/js/broadcast.js | 40 +- src/static/js/broadcast_revisions.js | 2 +- src/static/js/broadcast_slider.js | 4 +- src/static/js/pad.js | 2 +- src/static/js/timeslider.js | 5 +- 8 files changed, 359 insertions(+), 578 deletions(-) delete mode 100644 src/node/handler/TimesliderMessageHandler.js diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 4a570e21..194ee6d5 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -203,6 +203,10 @@ exports.handleMessage = function(client, message) { handleSuggestUserName(client, message); } + else if(message.type == "CHANGESET_REQ") + { + handleChangesetRequest(client, message); + } //if the message type is unknown, throw an exception else { @@ -210,6 +214,7 @@ exports.handleMessage = function(client, message) } } + /** * Handles a save revision message * @param client the client that send this message @@ -661,6 +666,7 @@ function handleClientReady(client, message) var authorColorId; var pad; var historicalAuthorData = {}; + var currentTime; var readOnlyId; var chatMessages; @@ -735,6 +741,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) { @@ -761,7 +777,6 @@ function handleClientReady(client, message) } ], callback); - }, function(callback) { @@ -813,7 +828,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", @@ -837,7 +853,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 @@ -858,7 +876,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(); } @@ -961,3 +979,325 @@ 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 padId = message.padId; + + //build the requested rough changesets and send them back + getChangesetInfo(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;rYou have no permission to access this pad") + } else { + console.warn("Unknown message type: " + message.type); + console.warn(["XXX", message]); } }); @@ -107,7 +110,7 @@ function sendSocketMsg(type, data) var sessionID = readCookie("sessionID"); var password = readCookie("password"); - var msg = { "component" : "timeslider", + var msg = { "component" : "pad", // FIXME: Remove this stupidity! "type": type, "data": data, "padId": padId, From 583743a3f9e2e0aae65b4feb420668a140d91ec4 Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Mon, 23 Apr 2012 12:58:04 +0200 Subject: [PATCH 02/23] Restored saved revision support --- src/node/handler/PadMessageHandler.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 194ee6d5..e17cfe2b 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -820,6 +820,7 @@ function handleClientReady(client, message) "initialOptions": { "guestPolicy": "deny" }, + "savedRevisions": pad.getSavedRevisions(), "collab_client_vars": { "initialAttributedText": atext, "clientIp": "127.0.0.1", From 1faae5b75618147f9efc5a6c7f9cdd9b84db5524 Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Mon, 23 Apr 2012 13:29:27 +0200 Subject: [PATCH 03/23] Timeslider autoupdate on pad change --- src/static/js/broadcast.js | 97 +++++++++++++++++++------------------ src/static/js/pad.js | 2 + src/static/js/timeslider.js | 7 +-- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js index 90b61361..f1024fc4 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -467,55 +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); diff --git a/src/static/js/pad.js b/src/static/js/pad.js index bdadd84e..0087cbe7 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -360,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/timeslider.js b/src/static/js/timeslider.js index 1f974e4e..63b2a27d 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -80,16 +80,11 @@ function init() { { handleClientVars(message); } - else if(message.type == "CHANGESET_REQ") - { - changesetLoader.handleSocketResponse(message); - } else if(message.accessStatus) { $("body").html("

You have no permission to access this pad

") } else { - console.warn("Unknown message type: " + message.type); - console.warn(["XXX", message]); + changesetLoader.handleMessageFromServer(message); } }); From e5a22423bb9d15a7a46764cdced466490b006f31 Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Mon, 23 Apr 2012 14:20:17 +0200 Subject: [PATCH 04/23] Fixed timedelta problem --- src/node/handler/PadMessageHandler.js | 45 +++++++++++++++++---------- src/static/js/broadcast.js | 2 +- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index e17cfe2b..56a8091c 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -517,17 +517,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) @@ -547,6 +545,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) { @@ -564,24 +571,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); } diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js index f1024fc4..cbb13fd7 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -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(); From ed7e254417dd3c4cab7e23b2ee85796b79cecbc7 Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Mon, 23 Apr 2012 14:47:07 +0200 Subject: [PATCH 05/23] Disconnect handling and cleanup of unused code --- src/static/js/broadcast.js | 110 +++--------------------------- src/static/js/broadcast_slider.js | 25 +++---- src/static/js/timeslider.js | 11 +++ src/templates/timeslider.html | 43 +++++++++++- 4 files changed, 71 insertions(+), 118 deletions(-) diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js index cbb13fd7..86e63f93 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -519,81 +519,6 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro } }; - 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 () @@ -614,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 = {}; diff --git a/src/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.js index f458a3a8..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(){ diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.js index 63b2a27d..098c52cb 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.js @@ -71,6 +71,11 @@ function init() { sendSocketMsg("CLIENT_READY", {}); }); + socket.on('disconnect', function() + { + BroadcastSlider.showReconnectUI(); + }); + //route the incoming messages socket.on('message', function(message) { @@ -96,6 +101,12 @@ function init() { } else { $("#returnbutton").attr("href", document.location.href.substring(0,document.location.href.lastIndexOf("/"))); } + + $('button#forcereconnect').click(function() + { + window.location.reload(); + }); + }); } diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index ca4bc6e9..9cea2c50 100644 --- a/src/templates/timeslider.html +++ b/src/templates/timeslider.html @@ -103,7 +103,48 @@ -
+
+ <% e.begin_block("modals"); %> +
+
+
Connecting...
+
Reestablishing connection...
+
+

Disconnected.

+

Opened in another window.

+

No Authorization.

+
+

We're having trouble talking to the EtherPad lite synchronization server. You may be connecting through an incompatible firewall or proxy server.

+
+
+

We were unable to connect to the EtherPad lite synchronization server. This may be due to an incompatibility with your web browser or internet connection.

+
+
+

You seem to have opened this pad in another browser window. If you'd like to use this window instead, you can reconnect.

+
+
+

Lost connection with the EtherPad lite synchronization server. This may be due to a loss of network connectivity.

+
+
+

Server not responding. This may be due to network connectivity issues or high load on the server.

+
+
+

Your browser's credentials or permissions have changed while viewing this pad. Try reconnecting.

+
+
+

This pad was deleted.

+
+
+

If this continues to happen, please let us know

+
+
+ +
+
+
+
+ <% e.end_block(); %> +
From 480d0b8e2506c70ae84f73a03095c3b9379ae5a4 Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Mon, 23 Apr 2012 16:18:14 +0200 Subject: [PATCH 06/23] ReadOnly pad handling --- src/node/db/ReadOnlyManager.js | 30 ++++++ src/node/handler/PadMessageHandler.js | 144 ++++++++++++-------------- 2 files changed, 98 insertions(+), 76 deletions(-) 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/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 56a8091c..724ec7da 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -34,19 +34,18 @@ var log4js = require('log4js'); var messageLogger = log4js.getLogger("message"); var _ = require('underscore'); -/** - * 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 +71,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 +99,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 +147,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]; } @@ -173,43 +170,28 @@ exports.handleMessage = function(client, message) } //Check what type of message we get and delegate to the other methodes - if(message.type == "CLIENT_READY") - { + if(message.type == "CLIENT_READY") { handleClientReady(client, message); - } - else if(message.type == "COLLABROOM" && - message.data.type == "USER_CHANGES") - { - handleUserChanges(client, message); - } - else if(message.type == "COLLABROOM" && - message.data.type == "USERINFO_UPDATE") - { - handleUserInfoUpdate(client, message); - } - else if(message.type == "COLLABROOM" && - message.data.type == "CHAT_MESSAGE") - { - handleChatMessage(client, message); - } - else if(message.type == "COLLABROOM" && - message.data.type == "SAVE_REVISION") - { - handleSaveRevisionMessage(client, message); - } - else if(message.type == "COLLABROOM" && - message.data.type == "CLIENT_MESSAGE" && - message.data.payload.type == "suggestUserName") - { - handleSuggestUserName(client, message); - } - else if(message.type == "CHANGESET_REQ") - { + } else if(message.type == "CHANGESET_REQ") { handleChangesetRequest(client, message); - } - //if the message type is unknown, throw an exception - else - { + } 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); } } @@ -221,7 +203,7 @@ exports.handleMessage = function(client, 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) @@ -242,7 +224,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; @@ -318,7 +300,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]) @@ -352,7 +334,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) @@ -398,7 +380,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); @@ -410,7 +392,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; @@ -680,14 +662,28 @@ function handleClientReady(client, message) var pad; var historicalAuthorData = {}; var currentTime; - var readOnlyId; 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; @@ -730,21 +726,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); }, @@ -794,30 +781,32 @@ function handleClientReady(client, message) 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); @@ -825,6 +814,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 @@ -853,9 +845,9 @@ 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, "serverTimestamp": new Date().getTime(), "globalPadId": message.padId, "userId": author, @@ -920,7 +912,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; From ba3430ebb7347e378cfb5512f24dd2c9968e2210 Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Mon, 23 Apr 2012 16:20:55 +0200 Subject: [PATCH 07/23] Corect ReadOnly link --- src/static/js/pad_editbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js index 95d19505..7335dfc5 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.js @@ -237,7 +237,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("