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