      1 /**
      2  * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS-IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16
     17 var padManager = require("./PadManager");
     18 var Changeset = require("./Changeset");
     19 var AttributePoolFactory = require("./AttributePoolFactory");
     20 var authorManager = require("./AuthorManager");
     21
     22 /**
     23  * A associative array that translates a session to a pad
     24  */
     25 var session2pad = {};
     26 /**
     27  * A associative array that saves which sessions belong to a pad
     28  */
     29 var pad2sessions = {};
     30
     31 /**
     32  * A associative array that saves some general informations about a session
     33  * key = sessionId
     34  * values = author, rev
     35  *   rev = That last revision that was send to this client
     36  *   author = the author name of this session
     37  */
     38 var sessioninfos = {};
     39
     40 /**
     41  * Saves the Socket class we need to send and recieve data from the client
     42  */
     43 var socketio;
     44
     45 /**
     46  * This Method is called by server.js to tell the message handler on which socket it should send
     47  * @param socket_io The Socket
     48  */
     49 exports.setSocketIO = function(socket_io)
     50 {
     51   socketio=socket_io;
     52 }
     53
     54 /**
     55  * Handles the connection of a new user
     56  * @param client the new client
     57  */
     58 exports.handleConnect = function(client)
     59 {
     60   //check if all ok
     61   throwExceptionIfClientOrIOisInvalid(client);
     62
     63   //Initalize session2pad and sessioninfos for this new session
     64   session2pad[client.sessionId]=null;
     65   sessioninfos[client.sessionId]={};
     66 }
     67
     68 /**
     69  * Handles the disconnection of a user
     70  * @param client the client that leaves
     71  */
     72 exports.handleDisconnect = function(client)
     73 {
     74   //check if all ok
     75   throwExceptionIfClientOrIOisInvalid(client);
     76
     77   //save the padname of this session
     78   var sessionPad=session2pad[client.sessionId];
     79
     80   //Go trough all sessions of this pad, search and destroy the entry of this client
     81   for(i in pad2sessions[sessionPad])
     82   {
     83     if(pad2sessions[sessionPad][i] == client.sessionId)
     84     {
     85       delete pad2sessions[sessionPad][i];
     86       break;
     87     }
     88   }
     89
     90   //Delete the session2pad and sessioninfos entrys of this session
     91   delete session2pad[client.sessionId];
     92   delete sessioninfos[client.sessionId];
     93 }
     94
     95 /**
     96  * Handles a message from a user
     97  * @param client the client that send this message
     98  * @param message the message from the client
     99  */
    100 exports.handleMessage = function(client, message)
    101 {
    102   //check if all ok
    103   throwExceptionIfClientOrIOisInvalid(client);
    104
    105   if(message == null)
    106   {
    107     throw "Message is null!";
    108   }
    109   //Etherpad sometimes send JSON and sometimes a JSONstring...
    110   if(typeof message == "string")
    111   {
    112     message = JSON.parse(message);
    113   }
    114   if(!message.type)
    115   {
    116     throw "Message have no type attribute!";
    117   }
    118
    119   //Check what type of message we get and delegate to the other methodes
    120   if(message.type == "CLIENT_READY")
    121   {
    122     handleClientReady(client, message);
    123   }
    124   else if(message.type == "COLLABROOM" &&
    125           message.data.type == "USER_CHANGES")
    126   {
    127     console.error(JSON.stringify(message));
    128     handleUserChanges(client, message);
    129   }
    130   else if(message.type == "COLLABROOM" &&
    131           message.data.type == "USERINFO_UPDATE")
    132   {
    133     console.error(JSON.stringify(message));
    134     handleUserInfoUpdate(client, message);
    135   }
    136   //if the message type is unkown, throw an exception
    137   else
    138   {
    139     console.error(message);
    140     throw "unkown Message Type: '" + message.type + "'";
    141   }
    142 }
    143
    144 /**
    145  * Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
    146  * @param client the client that send this message
    147  * @param message the message from the client
    148  */
    149 function handleUserInfoUpdate(client, message)
    150 {
    151   //check if all ok
    152   if(message.data.userInfo.name == null)
    153   {
    154     throw "USERINFO_UPDATE Message have no name!";
    155   }
    156   if(message.data.userInfo.colorId == null)
    157   {
    158     throw "USERINFO_UPDATE Message have no colorId!";
    159   }
    160
    161   //Find out the author name of this session
    162   var author = sessioninfos[client.sessionId].author;
    163
    164   //Tell the authorManager about the new attributes
    165   authorManager.setAuthorColorId(author, message.data.userInfo.colorId);
    166   authorManager.setAuthorName(author, message.data.userInfo.name);
    167 }
    168
    169 /**
    170  * Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
    171  * This Method is nearly 90% copied out of the Etherpad Source Code. So I can't tell you what happens here exactly
    172  * Look at https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges()
    173  * @param client the client that send this message
    174  * @param message the message from the client
    175  */
    176 function handleUserChanges(client, message)
    177 {
    178   //check if all ok
    179   if(message.data.baseRev == null)
    180   {
    181     throw "USER_CHANGES Message have no baseRev!";
    182   }
    183   if(message.data.apool == null)
    184   {
    185     throw "USER_CHANGES Message have no apool!";
    186   }
    187   if(message.data.changeset == null)
    188   {
    189     throw "USER_CHANGES Message have no changeset!";
    190   }
    191
    192   //get all Vars we need
    193   var baseRev = message.data.baseRev;
    194   var wireApool = (AttributePoolFactory.createAttributePool()).fromJsonable(message.data.apool);
    195   var changeset = message.data.changeset;
    196   var pad = padManager.getPad(session2pad[client.sessionId], false);
    197
    198   //ex. _checkChangesetAndPool
    199
    200   //Copied from Etherpad, don't know what it does exactly
    201   Changeset.checkRep(changeset);
    202   Changeset.eachAttribNumber(changeset, function(n) {
    203     if (! wireApool.getAttrib(n)) {
    204       throw "Attribute pool is missing attribute "+n+" for changeset "+changeset;
    205     }
    206   });
    207
    208   //ex. adoptChangesetAttribs
    209
    210   //Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
    211   Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool());
    212
    213   //ex. applyUserChanges
    214
    215   var apool = pad.pool();
    216   var r = baseRev;
    217
    218   while (r < pad.getHeadRevisionNumber()) {
    219     r++;
    220     var c = pad.getRevisionChangeset(r);
    221     changeset = Changeset.follow(c, changeset, false, apool);
    222   }
    223
    224   var prevText = pad.text();
    225   if (Changeset.oldLen(changeset) != prevText.length) {
    226     throw "Can't apply USER_CHANGES "+changeset+" with oldLen "
    227     + Changeset.oldLen(changeset) + " to document of length " + prevText.length;
    228   }
    229
    230   var thisAuthor = sessioninfos[client.sessionId].author;
    231
    232   pad.appendRevision(changeset, thisAuthor);
    233
    234   var correctionChangeset = _correctMarkersInPad(pad.atext(), pad.pool());
    235   if (correctionChangeset) {
    236     pad.appendRevision(correctionChangeset);
    237   }
    238
    239   if (pad.text().lastIndexOf("\n\n") != pad.text().length-2) {
    240     var nlChangeset = Changeset.makeSplice(
    241       pad.text(), pad.text().length-1, 0, "\n");
    242     pad.appendRevision(nlChangeset);
    243   }
    244
    245   console.error(JSON.stringify(pad.pool()));
    246
    247   //ex. updatePadClients
    248
    249   for(i in pad2sessions[pad.id])
    250   {
    251     var session = pad2sessions[pad.id][i];
    252     var lastRev = sessioninfos[session].rev;
    253
    254     while (lastRev < pad.getHeadRevisionNumber())
    255     {
    256       var r = ++lastRev;
    257       var author = pad.getRevisionAuthor(r);
    258
    259       if(author == sessioninfos[session].author)
    260       {
    261         socketio.clients[session].send({"type":"COLLABROOM","data":{type:"ACCEPT_COMMIT", newRev:r}});
    262       }
    263       else
    264       {
    265         var forWire = Changeset.prepareForWire(pad.getRevisionChangeset(r), pad.pool());
    266         var wireMsg = {"type":"COLLABROOM","data":{type:"NEW_CHANGES", newRev:r,
    267                    changeset: forWire.translated,
    268                    apool: forWire.pool,
    269                    author: author}};
    270         socketio.clients[session].send(wireMsg);
    271       }
    272     }
    273
    274     sessioninfos[session].rev = pad.getHeadRevisionNumber();
    275   }
    276 }
    277
    278 /**
    279  * Copied from the Etherpad Source Code. Don't know what this methode does excatly...
    280  */
    281 function _correctMarkersInPad(atext, apool) {
    282   var text = atext.text;
    283
    284   // collect char positions of line markers (e.g. bullets) in new atext
    285   // that aren't at the start of a line
    286   var badMarkers = [];
    287   var iter = Changeset.opIterator(atext.attribs);
    288   var offset = 0;
    289   while (iter.hasNext()) {
    290     var op = iter.next();
    291     var listValue = Changeset.opAttributeValue(op, 'list', apool);
    292     if (listValue) {
    293       for(var i=0;i<op.chars;i++) {
    294         if (offset > 0 && text.charAt(offset-1) != '\n') {
    295           badMarkers.push(offset);
    296         }
    297         offset++;
    298       }
    299     }
    300     else {
    301       offset += op.chars;
    302     }
    303   }
    304
    305   if (badMarkers.length == 0) {
    306     return null;
    307   }
    308
    309   // create changeset that removes these bad markers
    310   offset = 0;
    311   var builder = Changeset.builder(text.length);
    312   badMarkers.forEach(function(pos) {
    313     builder.keepText(text.substring(offset, pos));
    314     builder.remove(1);
    315     offset = pos+1;
    316   });
    317   return builder.toString();
    318 }
    319
    320 /**
    321  * Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token
    322  * and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad
    323  * @param client the client that send this message
    324  * @param message the message from the client
    325  */
    326 function handleClientReady(client, message)
    327 {
    328   //check if all ok
    329   if(!message.token)
    330   {
    331     throw "CLIENT_READY Message have no token!";
    332   }
    333   if(!message.padId)
    334   {
    335     throw "CLIENT_READY Message have no padId!";
    336   }
    337   if(!message.protocolVersion)
    338   {
    339     throw "CLIENT_READY Message have no protocolVersion!";
    340   }
    341   if(message.protocolVersion != 1)
    342   {
    343     throw "CLIENT_READY Message have a unkown protocolVersion '" + protocolVersion + "'!";
    344   }
    345
    346   //Ask the author Manager for a authorname of this token.
    347   var author = authorManager.getAuthor4Token(message.token);
    348
    349   //Save in session2pad that this session belonges to this pad
    350   var sessionId=String(client.sessionId);
    351   session2pad[sessionId] = message.padId;
    352
    353   //check if there is already a pad2sessions entry, if not, create one
    354   if(!pad2sessions[message.padId])
    355   {
    356     pad2sessions[message.padId] = [];
    357   }
    358
    359   //Saves in pad2sessions that this session belongs to this pad
    360   pad2sessions[message.padId].push(sessionId);
    361
    362   //Tell the PadManager that it should ensure that this Pad exist
    363   padManager.ensurePadExists(message.padId);
    364
    365   //Ask the PadManager for a function Wrapper for this Pad
    366   var pad = padManager.getPad(message.padId, false);
    367
    368   //prepare all values for the wire
    369   atext = pad.atext();
    370   var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool());
    371   var apool = attribsForWire.pool.toJsonable();
    372   atext.attribs = attribsForWire.translated;
    373
    374   var clientVars = {
    375     "accountPrivs": {
    376         "maxRevisions": 100
    377     },
    378     "initialRevisionList": [],
    379     "initialOptions": {
    380         "guestPolicy": "deny"
    381     },
    382     "collab_client_vars": {
    383         "initialAttributedText": atext,
    384         "clientIp": client.request.connection.remoteAddress,
    385         //"clientAgent": "Anonymous Agent",
    386         "padId": message.padId,
    387         "historicalAuthorData": {},
    388         "apool": apool,
    389         "rev": pad.getHeadRevisionNumber(),
    390         "globalPadId": message.padId
    391     },
    392     "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"],
    393     "clientIp": client.request.connection.remoteAddress,
    394     "userIsGuest": true,
    395     "userColor": authorManager.getAuthorColorId(author),
    396     "padId": message.padId,
    397     "initialTitle": "Pad: " + message.padId,
    398     "opts": {},
    399     "chatHistory": {
    400         "start": 0,
    401         "historicalAuthorData": {},
    402         "end": 0,
    403         "lines": []
    404     },
    405     "numConnectedUsers": pad2sessions[message.padId].length,
    406     "isProPad": false,
    407     "serverTimestamp": new Date().getTime(),
    408     "globalPadId": message.padId,
    409     "userId": author,
    410     "cookiePrefsToSet": {
    411         "fullWidth": false,
    412         "hideSidebar": false
    413     },
    414     "hooks": {}
    415   }
    416
    417   //Add a username to the clientVars if one avaiable
    418   if(authorManager.getAuthorName(author) != null)
    419   {
    420     clientVars.userName = authorManager.getAuthorName(author);
    421   }
    422
    423   //Add all authors that worked on this pad, to the historicalAuthorData on clientVars
    424   var allAuthors = pad.getAllAuthors();
    425   for(i in allAuthors)
    426   {
    427     clientVars.collab_client_vars.historicalAuthorData[allAuthors[i]] = {};
    428     if(authorManager.getAuthorName(author) != null)
    429       clientVars.collab_client_vars.historicalAuthorData[allAuthors[i]].name = authorManager.getAuthorName(author);
    430     clientVars.collab_client_vars.historicalAuthorData[allAuthors[i]].colorId = authorManager.getAuthorColorId(author);
    431   }
    432
    433   //Send the clientVars to the Client
    434   client.send(clientVars);
    435
    436   //Save the revision and the author id in sessioninfos
    437   sessioninfos[client.sessionId].rev = pad.getHeadRevisionNumber();
    438   sessioninfos[client.sessionId].author = author;
    439
    440   //prepare the notification for the other users on the pad, that this user joined
    441   var messageToTheOtherUsers = {
    442     "type": "COLLABROOM",
    443     "data": {
    444       type: "USER_NEWINFO",
    445       userInfo: {
    446         "ip": "127.0.0.1",
    447         "colorId": authorManager.getAuthorColorId(author),
    448         "userAgent": "Anonymous",
    449         "userId": author
    450       }
    451     }
    452   };
    453
    454   //Add the authorname of this new User, if avaiable
    455   if(authorManager.getAuthorName(author) != null)
    456   {
    457     messageToTheOtherUsers.data.userInfo.name = authorManager.getAuthorName(author);
    458   }
    459
    460   //
    461   for(i in pad2sessions[message.padId])
    462   {
    463     if(pad2sessions[message.padId][i] != client.sessionId)
    464     {
    465       socketio.clients[pad2sessions[message.padId][i]].send(messageToTheOtherUsers);
    466
    467       var messageToNotifyTheClientAboutTheOthers = {
    468         "type": "COLLABROOM",
    469         "data": {
    470           type: "USER_NEWINFO",
    471           userInfo: {
    472             "ip": "127.0.0.1",
    473             "colorId": authorManager.getAuthorColorId(sessioninfos[pad2sessions[message.padId][i]].author),
    474             "userAgent": "Anonymous",
    475             "userId": sessioninfos[pad2sessions[message.padId][i]].author
    476           }
    477         }
    478       };
    479
    480       client.send(messageToNotifyTheClientAboutTheOthers);
    481     }
    482   }
    483
    484
    485 }
    486
    487 /**
    488  * A internal function that simply checks if client or socketio is null and throws a exception if yes
    489  */
    490 function throwExceptionIfClientOrIOisInvalid(client)
    491 {
    492   if(client == null)
    493   {
    494     throw "Client is null!";
    495   }
    496   if(socketio == null)
    497   {
    498     throw "SocketIO is not set or null! Please use setSocketIO(io) to set it";
    499   }
    500 }
    501