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