2011-03-26 13:10:41 +00:00
|
|
|
/**
|
2011-05-30 14:53:11 +00:00
|
|
|
* The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
2011-08-11 14:26:41 +00:00
|
|
|
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
2011-03-26 13:10:41 +00:00
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS-IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2011-05-16 12:10:53 +00:00
|
|
|
var async = require("async");
|
2011-07-27 17:52:23 +00:00
|
|
|
var padManager = require("../db/PadManager");
|
|
|
|
var Changeset = require("../utils/Changeset");
|
|
|
|
var AttributePoolFactory = require("../utils/AttributePoolFactory");
|
|
|
|
var authorManager = require("../db/AuthorManager");
|
|
|
|
var readOnlyManager = require("../db/ReadOnlyManager");
|
|
|
|
var settings = require('../utils/Settings');
|
2011-08-13 21:07:21 +00:00
|
|
|
var securityManager = require("../db/SecurityManager");
|
2011-08-17 16:28:30 +00:00
|
|
|
var log4js = require('log4js');
|
2011-08-21 19:09:25 +00:00
|
|
|
var os = require("os");
|
2011-08-17 16:28:30 +00:00
|
|
|
var messageLogger = log4js.getLogger("message");
|
2011-03-26 13:10:41 +00:00
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
/**
|
|
|
|
* A associative array that translates a session to a pad
|
|
|
|
*/
|
2011-03-26 13:10:41 +00:00
|
|
|
var session2pad = {};
|
2011-03-26 21:29:33 +00:00
|
|
|
/**
|
|
|
|
* A associative array that saves which sessions belong to a pad
|
|
|
|
*/
|
2011-03-26 13:10:41 +00:00
|
|
|
var pad2sessions = {};
|
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
/**
|
|
|
|
* A associative array that saves some general informations about a session
|
|
|
|
* key = sessionId
|
|
|
|
* values = author, rev
|
|
|
|
* rev = That last revision that was send to this client
|
|
|
|
* author = the author name of this session
|
|
|
|
*/
|
2011-03-26 13:10:41 +00:00
|
|
|
var sessioninfos = {};
|
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
/**
|
|
|
|
* Saves the Socket class we need to send and recieve data from the client
|
|
|
|
*/
|
2011-03-26 13:10:41 +00:00
|
|
|
var socketio;
|
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
/**
|
|
|
|
* This Method is called by server.js to tell the message handler on which socket it should send
|
|
|
|
* @param socket_io The Socket
|
|
|
|
*/
|
2011-03-26 13:10:41 +00:00
|
|
|
exports.setSocketIO = function(socket_io)
|
|
|
|
{
|
|
|
|
socketio=socket_io;
|
|
|
|
}
|
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
/**
|
|
|
|
* Handles the connection of a new user
|
|
|
|
* @param client the new client
|
|
|
|
*/
|
2011-03-26 13:10:41 +00:00
|
|
|
exports.handleConnect = function(client)
|
2011-08-17 16:28:30 +00:00
|
|
|
{
|
2011-03-26 21:29:33 +00:00
|
|
|
//Initalize session2pad and sessioninfos for this new session
|
2011-06-23 13:08:18 +00:00
|
|
|
session2pad[client.id]=null;
|
|
|
|
sessioninfos[client.id]={};
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
|
2011-08-16 19:02:30 +00:00
|
|
|
/**
|
|
|
|
* Kicks all sessions from a pad
|
|
|
|
* @param client the new client
|
|
|
|
*/
|
|
|
|
exports.kickSessionsFromPad = function(padID)
|
|
|
|
{
|
|
|
|
//skip if there is nobody on this pad
|
|
|
|
if(!pad2sessions[padID])
|
|
|
|
return;
|
|
|
|
|
|
|
|
//disconnect everyone from this pad
|
|
|
|
for(var i in pad2sessions[padID])
|
|
|
|
{
|
|
|
|
socketio.sockets.sockets[pad2sessions[padID][i]].json.send({disconnect:"deleted"});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
/**
|
|
|
|
* Handles the disconnection of a user
|
|
|
|
* @param client the client that leaves
|
|
|
|
*/
|
2011-03-26 13:10:41 +00:00
|
|
|
exports.handleDisconnect = function(client)
|
2011-08-17 16:28:30 +00:00
|
|
|
{
|
2011-03-26 21:29:33 +00:00
|
|
|
//save the padname of this session
|
2011-06-23 13:08:18 +00:00
|
|
|
var sessionPad=session2pad[client.id];
|
2011-03-26 13:10:41 +00:00
|
|
|
|
2011-05-23 19:11:57 +00:00
|
|
|
//if this connection was already etablished with a handshake, send a disconnect message to the others
|
2011-07-31 14:05:04 +00:00
|
|
|
if(sessioninfos[client.id] && sessioninfos[client.id].author)
|
2011-05-16 12:10:53 +00:00
|
|
|
{
|
2011-06-23 13:08:18 +00:00
|
|
|
var author = sessioninfos[client.id].author;
|
2011-05-23 19:11:57 +00:00
|
|
|
|
|
|
|
//get the author color out of the db
|
|
|
|
authorManager.getAuthorColorId(author, function(err, color)
|
2011-05-16 12:10:53 +00:00
|
|
|
{
|
2011-05-23 19:11:57 +00:00
|
|
|
if(err) throw err;
|
|
|
|
|
|
|
|
//prepare the notification for the other users on the pad, that this user left
|
|
|
|
var messageToTheOtherUsers = {
|
|
|
|
"type": "COLLABROOM",
|
|
|
|
"data": {
|
|
|
|
type: "USER_LEAVE",
|
|
|
|
userInfo: {
|
|
|
|
"ip": "127.0.0.1",
|
|
|
|
"colorId": color,
|
|
|
|
"userAgent": "Anonymous",
|
|
|
|
"userId": author
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
//Go trough all user that are still on the pad, and send them the USER_LEAVE message
|
|
|
|
for(i in pad2sessions[sessionPad])
|
2011-05-16 12:10:53 +00:00
|
|
|
{
|
2011-06-23 13:08:18 +00:00
|
|
|
socketio.sockets.sockets[pad2sessions[sessionPad][i]].json.send(messageToTheOtherUsers);
|
2011-03-28 22:07:35 +00:00
|
|
|
}
|
2011-05-23 19:11:57 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
//Go trough all sessions of this pad, search and destroy the entry of this client
|
|
|
|
for(i in pad2sessions[sessionPad])
|
|
|
|
{
|
2011-06-23 13:08:18 +00:00
|
|
|
if(pad2sessions[sessionPad][i] == client.id)
|
2011-03-26 13:10:41 +00:00
|
|
|
{
|
2011-05-23 19:11:57 +00:00
|
|
|
delete pad2sessions[sessionPad][i];
|
|
|
|
break;
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
2011-05-23 19:11:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//Delete the session2pad and sessioninfos entrys of this session
|
2011-06-23 13:08:18 +00:00
|
|
|
delete session2pad[client.id];
|
|
|
|
delete sessioninfos[client.id];
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
/**
|
|
|
|
* Handles a message from a user
|
|
|
|
* @param client the client that send this message
|
|
|
|
* @param message the message from the client
|
|
|
|
*/
|
2011-03-26 13:10:41 +00:00
|
|
|
exports.handleMessage = function(client, message)
|
2011-03-26 21:29:33 +00:00
|
|
|
{
|
2011-03-26 13:10:41 +00:00
|
|
|
if(message == null)
|
|
|
|
{
|
2011-08-20 17:56:38 +00:00
|
|
|
messageLogger.warn("Message is null!");
|
|
|
|
return;
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
if(!message.type)
|
|
|
|
{
|
2011-08-20 17:56:38 +00:00
|
|
|
messageLogger.warn("Message has no type attribute!");
|
|
|
|
return;
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
//Check what type of message we get and delegate to the other methodes
|
2011-03-26 13:10:41 +00:00
|
|
|
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);
|
|
|
|
}
|
2011-07-14 15:15:38 +00:00
|
|
|
else if(message.type == "COLLABROOM" &&
|
|
|
|
message.data.type == "CHAT_MESSAGE")
|
|
|
|
{
|
|
|
|
handleChatMessage(client, message);
|
|
|
|
}
|
2011-06-30 13:19:30 +00:00
|
|
|
else if(message.type == "COLLABROOM" &&
|
|
|
|
message.data.type == "CLIENT_MESSAGE" &&
|
|
|
|
message.data.payload.type == "suggestUserName")
|
|
|
|
{
|
|
|
|
handleSuggestUserName(client, message);
|
|
|
|
}
|
2011-03-26 21:29:33 +00:00
|
|
|
//if the message type is unkown, throw an exception
|
2011-03-26 13:10:41 +00:00
|
|
|
else
|
|
|
|
{
|
2011-08-17 16:28:30 +00:00
|
|
|
messageLogger.warn("Droped message, unkown Message Type " + message.type);
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-07-14 15:15:38 +00:00
|
|
|
/**
|
|
|
|
* Handles a Chat Message
|
|
|
|
* @param client the client that send this message
|
|
|
|
* @param message the message from the client
|
|
|
|
*/
|
|
|
|
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 pad;
|
|
|
|
var userName;
|
|
|
|
|
|
|
|
async.series([
|
|
|
|
//get the pad
|
|
|
|
function(callback)
|
|
|
|
{
|
|
|
|
padManager.getPad(padId, function(err, _pad)
|
|
|
|
{
|
|
|
|
pad = _pad;
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
function(callback)
|
|
|
|
{
|
|
|
|
authorManager.getAuthorName(userId, function(err, _userName)
|
|
|
|
{
|
|
|
|
userName = _userName;
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
//save the chat message and broadcast it
|
|
|
|
function(callback)
|
|
|
|
{
|
|
|
|
//save the chat message
|
|
|
|
pad.appendChatMessage(text, userId, time);
|
|
|
|
|
|
|
|
var msg = {
|
|
|
|
type: "COLLABROOM",
|
|
|
|
data: {
|
|
|
|
type: "CHAT_MESSAGE",
|
|
|
|
userId: userId,
|
|
|
|
userName: userName,
|
|
|
|
time: time,
|
|
|
|
text: text
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
//broadcast the chat message to everyone on the pad
|
|
|
|
for(var i in pad2sessions[padId])
|
|
|
|
{
|
|
|
|
socketio.sockets.sockets[pad2sessions[padId][i]].json.send(msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
], function(err)
|
|
|
|
{
|
|
|
|
if(err) throw err;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2011-06-30 13:19:30 +00:00
|
|
|
/**
|
|
|
|
* Handles a handleSuggestUserName, that means a user have suggest a userName for a other user
|
|
|
|
* @param client the client that send this message
|
|
|
|
* @param message the message from the client
|
|
|
|
*/
|
|
|
|
function handleSuggestUserName(client, message)
|
|
|
|
{
|
|
|
|
//check if all ok
|
|
|
|
if(message.data.payload.newName == null)
|
|
|
|
{
|
2011-08-17 16:28:30 +00:00
|
|
|
messageLogger.warn("Droped message, suggestUserName Message has no newName!");
|
|
|
|
return;
|
2011-06-30 13:19:30 +00:00
|
|
|
}
|
|
|
|
if(message.data.payload.unnamedId == null)
|
|
|
|
{
|
2011-08-17 16:28:30 +00:00
|
|
|
messageLogger.warn("Droped message, suggestUserName Message has no unnamedId!");
|
|
|
|
return;
|
2011-06-30 13:19:30 +00:00
|
|
|
}
|
|
|
|
|
2011-07-05 15:07:09 +00:00
|
|
|
var padId = session2pad[client.id];
|
2011-06-30 13:19:30 +00:00
|
|
|
|
|
|
|
//search the author and send him this message
|
|
|
|
for(var i in pad2sessions[padId])
|
|
|
|
{
|
|
|
|
if(sessioninfos[pad2sessions[padId][i]].author == message.data.payload.unnamedId)
|
|
|
|
{
|
2011-07-06 20:43:28 +00:00
|
|
|
socketio.sockets.sockets[pad2sessions[padId][i]].send(message);
|
2011-06-30 13:19:30 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
/**
|
|
|
|
* Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
|
|
|
|
* @param client the client that send this message
|
|
|
|
* @param message the message from the client
|
|
|
|
*/
|
2011-03-26 13:10:41 +00:00
|
|
|
function handleUserInfoUpdate(client, message)
|
|
|
|
{
|
2011-03-26 21:29:33 +00:00
|
|
|
//check if all ok
|
2011-03-26 13:10:41 +00:00
|
|
|
if(message.data.userInfo.colorId == null)
|
|
|
|
{
|
2011-08-17 16:28:30 +00:00
|
|
|
messageLogger.warn("Droped message, USERINFO_UPDATE Message has no colorId!");
|
|
|
|
return;
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
//Find out the author name of this session
|
2011-06-23 13:08:18 +00:00
|
|
|
var author = sessioninfos[client.id].author;
|
2011-03-26 13:10:41 +00:00
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
//Tell the authorManager about the new attributes
|
2011-03-26 13:10:41 +00:00
|
|
|
authorManager.setAuthorColorId(author, message.data.userInfo.colorId);
|
|
|
|
authorManager.setAuthorName(author, message.data.userInfo.name);
|
2011-03-27 12:13:46 +00:00
|
|
|
|
2011-06-23 13:08:18 +00:00
|
|
|
var padId = session2pad[client.id];
|
2011-03-27 12:13:46 +00:00
|
|
|
|
|
|
|
//set a null name, when there is no name set. cause the client wants it null
|
|
|
|
if(message.data.userInfo.name == null)
|
|
|
|
{
|
|
|
|
message.data.userInfo.name = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
//The Client don't know about a USERINFO_UPDATE, it can handle only new user_newinfo, so change the message type
|
|
|
|
message.data.type = "USER_NEWINFO";
|
|
|
|
|
|
|
|
//Send the other clients on the pad the update message
|
2011-06-30 13:19:30 +00:00
|
|
|
for(var i in pad2sessions[padId])
|
2011-03-27 12:13:46 +00:00
|
|
|
{
|
2011-06-23 13:08:18 +00:00
|
|
|
if(pad2sessions[padId][i] != client.id)
|
2011-03-27 12:13:46 +00:00
|
|
|
{
|
2011-07-06 17:34:39 +00:00
|
|
|
socketio.sockets.sockets[pad2sessions[padId][i]].json.send(message);
|
2011-03-27 12:13:46 +00:00
|
|
|
}
|
|
|
|
}
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
/**
|
|
|
|
* Handles a USERINFO_UPDATE, that means that a user have changed his color or name. Anyway, we get both informations
|
|
|
|
* This Method is nearly 90% copied out of the Etherpad Source Code. So I can't tell you what happens here exactly
|
|
|
|
* Look at https://github.com/ether/pad/blob/master/etherpad/src/etherpad/collab/collab_server.js in the function applyUserChanges()
|
|
|
|
* @param client the client that send this message
|
|
|
|
* @param message the message from the client
|
|
|
|
*/
|
2011-03-26 13:10:41 +00:00
|
|
|
function handleUserChanges(client, message)
|
|
|
|
{
|
2011-03-26 21:29:33 +00:00
|
|
|
//check if all ok
|
2011-03-26 13:10:41 +00:00
|
|
|
if(message.data.baseRev == null)
|
|
|
|
{
|
2011-08-17 16:28:30 +00:00
|
|
|
messageLogger.warn("Droped message, USER_CHANGES Message has no baseRev!");
|
|
|
|
return;
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
if(message.data.apool == null)
|
|
|
|
{
|
2011-08-17 16:28:30 +00:00
|
|
|
messageLogger.warn("Droped message, USER_CHANGES Message has no apool!");
|
|
|
|
return;
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
if(message.data.changeset == null)
|
|
|
|
{
|
2011-08-17 16:28:30 +00:00
|
|
|
messageLogger.warn("Droped message, USER_CHANGES Message has no changeset!");
|
|
|
|
return;
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
//get all Vars we need
|
2011-03-26 13:10:41 +00:00
|
|
|
var baseRev = message.data.baseRev;
|
|
|
|
var wireApool = (AttributePoolFactory.createAttributePool()).fromJsonable(message.data.apool);
|
|
|
|
var changeset = message.data.changeset;
|
2011-05-17 15:33:54 +00:00
|
|
|
|
|
|
|
var r, apool, pad;
|
2011-03-26 13:10:41 +00:00
|
|
|
|
2011-05-17 15:33:54 +00:00
|
|
|
async.series([
|
|
|
|
//get the pad
|
|
|
|
function(callback)
|
2011-03-26 13:10:41 +00:00
|
|
|
{
|
2011-06-23 13:08:18 +00:00
|
|
|
padManager.getPad(session2pad[client.id], function(err, value)
|
2011-03-26 13:10:41 +00:00
|
|
|
{
|
2011-05-17 15:33:54 +00:00
|
|
|
pad = value;
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
//create the changeset
|
|
|
|
function(callback)
|
|
|
|
{
|
|
|
|
//ex. _checkChangesetAndPool
|
|
|
|
|
|
|
|
//Copied from Etherpad, don't know what it does exactly
|
2011-08-20 17:51:43 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
//this looks like a changeset check, it throws errors sometimes
|
|
|
|
Changeset.checkRep(changeset);
|
|
|
|
|
|
|
|
Changeset.eachAttribNumber(changeset, function(n) {
|
|
|
|
if (! wireApool.getAttrib(n)) {
|
|
|
|
throw "Attribute pool is missing attribute "+n+" for changeset "+changeset;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
//there is an error in this changeset, so just refuse it
|
|
|
|
catch(e)
|
|
|
|
{
|
|
|
|
console.warn("Can't apply USER_CHANGES "+changeset+", cause it faild checkRep");
|
|
|
|
client.json.send({disconnect:"badChangeset"});
|
|
|
|
return;
|
|
|
|
}
|
2011-05-17 15:33:54 +00:00
|
|
|
|
|
|
|
//ex. adoptChangesetAttribs
|
|
|
|
|
|
|
|
//Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
|
|
|
|
changeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool);
|
|
|
|
|
|
|
|
//ex. applyUserChanges
|
|
|
|
apool = pad.pool;
|
|
|
|
r = baseRev;
|
|
|
|
|
|
|
|
//https://github.com/caolan/async#whilst
|
|
|
|
async.whilst(
|
|
|
|
function() { return r < pad.getHeadRevisionNumber(); },
|
|
|
|
function(callback)
|
|
|
|
{
|
|
|
|
r++;
|
|
|
|
|
|
|
|
pad.getRevisionChangeset(r, function(err, c)
|
|
|
|
{
|
|
|
|
if(err)
|
|
|
|
{
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
changeset = Changeset.follow(c, changeset, false, apool);
|
|
|
|
callback(null);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
//use the callback of the series function
|
|
|
|
callback
|
|
|
|
);
|
|
|
|
},
|
|
|
|
//do correction changesets, and send it to all users
|
|
|
|
function (callback)
|
|
|
|
{
|
|
|
|
var prevText = pad.text();
|
2011-07-30 12:33:16 +00:00
|
|
|
|
|
|
|
if (Changeset.oldLen(changeset) != prevText.length)
|
|
|
|
{
|
2011-07-31 17:25:51 +00:00
|
|
|
console.warn("Can't apply USER_CHANGES "+changeset+" with oldLen " + Changeset.oldLen(changeset) + " to document of length " + prevText.length);
|
2011-07-30 12:33:16 +00:00
|
|
|
client.json.send({disconnect:"badChangeset"});
|
|
|
|
callback();
|
|
|
|
return;
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
2011-05-17 15:33:54 +00:00
|
|
|
|
2011-06-23 13:08:18 +00:00
|
|
|
var thisAuthor = sessioninfos[client.id].author;
|
2011-05-17 15:33:54 +00:00
|
|
|
|
|
|
|
pad.appendRevision(changeset, thisAuthor);
|
|
|
|
|
|
|
|
var correctionChangeset = _correctMarkersInPad(pad.atext, pad.pool);
|
|
|
|
if (correctionChangeset) {
|
|
|
|
pad.appendRevision(correctionChangeset);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pad.text().lastIndexOf("\n\n") != pad.text().length-2) {
|
|
|
|
var nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length-1, 0, "\n");
|
|
|
|
pad.appendRevision(nlChangeset);
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
2011-05-17 15:33:54 +00:00
|
|
|
|
2011-07-21 19:13:58 +00:00
|
|
|
exports.updatePadClients(pad, callback);
|
|
|
|
}
|
|
|
|
], function(err)
|
|
|
|
{
|
|
|
|
if(err) throw err;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.updatePadClients = function(pad, callback)
|
|
|
|
{
|
2011-08-19 20:35:31 +00:00
|
|
|
//skip this step if noone is on this pad
|
|
|
|
if(!pad2sessions[pad.id])
|
|
|
|
{
|
|
|
|
callback();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2011-07-21 19:13:58 +00:00
|
|
|
//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(callback)
|
2011-05-17 15:33:54 +00:00
|
|
|
{
|
2011-07-21 19:13:58 +00:00
|
|
|
var author, revChangeset;
|
|
|
|
|
|
|
|
var r = ++lastRev;
|
|
|
|
|
|
|
|
async.parallel([
|
|
|
|
function (callback)
|
2011-05-17 15:33:54 +00:00
|
|
|
{
|
2011-07-21 19:13:58 +00:00
|
|
|
pad.getRevisionAuthor(r, function(err, value)
|
2011-05-17 15:33:54 +00:00
|
|
|
{
|
2011-07-21 19:13:58 +00:00
|
|
|
author = value;
|
|
|
|
callback(err);
|
2011-05-17 15:33:54 +00:00
|
|
|
});
|
|
|
|
},
|
2011-07-21 19:13:58 +00:00
|
|
|
function (callback)
|
|
|
|
{
|
|
|
|
pad.getRevisionChangeset(r, function(err, value)
|
|
|
|
{
|
|
|
|
revChangeset = value;
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
], function(err)
|
|
|
|
{
|
|
|
|
if(err)
|
|
|
|
{
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(author == sessioninfos[session].author)
|
|
|
|
{
|
|
|
|
socketio.sockets.sockets[session].json.send({"type":"COLLABROOM","data":{type:"ACCEPT_COMMIT", newRev:r}});
|
|
|
|
}
|
|
|
|
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}};
|
|
|
|
|
|
|
|
socketio.sockets.sockets[session].json.send(wireMsg);
|
|
|
|
}
|
2011-05-17 15:33:54 +00:00
|
|
|
|
2011-07-21 19:13:58 +00:00
|
|
|
callback(null);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
callback
|
|
|
|
);
|
|
|
|
|
|
|
|
sessioninfos[session].rev = pad.getHeadRevisionNumber();
|
|
|
|
},callback);
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
/**
|
|
|
|
* Copied from the Etherpad Source Code. Don't know what this methode does excatly...
|
|
|
|
*/
|
2011-03-26 13:10:41 +00:00
|
|
|
function _correctMarkersInPad(atext, apool) {
|
|
|
|
var text = atext.text;
|
|
|
|
|
|
|
|
// collect char positions of line markers (e.g. bullets) in new atext
|
|
|
|
// that aren't at the start of a line
|
|
|
|
var badMarkers = [];
|
|
|
|
var iter = Changeset.opIterator(atext.attribs);
|
|
|
|
var offset = 0;
|
|
|
|
while (iter.hasNext()) {
|
|
|
|
var op = iter.next();
|
|
|
|
var listValue = Changeset.opAttributeValue(op, 'list', apool);
|
|
|
|
if (listValue) {
|
|
|
|
for(var i=0;i<op.chars;i++) {
|
|
|
|
if (offset > 0 && text.charAt(offset-1) != '\n') {
|
|
|
|
badMarkers.push(offset);
|
|
|
|
}
|
|
|
|
offset++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
offset += op.chars;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (badMarkers.length == 0) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// create changeset that removes these bad markers
|
|
|
|
offset = 0;
|
|
|
|
var builder = Changeset.builder(text.length);
|
|
|
|
badMarkers.forEach(function(pos) {
|
|
|
|
builder.keepText(text.substring(offset, pos));
|
|
|
|
builder.remove(1);
|
|
|
|
offset = pos+1;
|
|
|
|
});
|
|
|
|
return builder.toString();
|
|
|
|
}
|
|
|
|
|
2011-03-26 21:29:33 +00:00
|
|
|
/**
|
|
|
|
* Handles a CLIENT_READY. A CLIENT_READY is the first message from the client to the server. The Client sends his token
|
|
|
|
* and the pad it wants to enter. The Server answers with the inital values (clientVars) of the pad
|
|
|
|
* @param client the client that send this message
|
|
|
|
* @param message the message from the client
|
|
|
|
*/
|
2011-03-26 13:10:41 +00:00
|
|
|
function handleClientReady(client, message)
|
|
|
|
{
|
2011-03-26 21:29:33 +00:00
|
|
|
//check if all ok
|
2011-03-26 13:10:41 +00:00
|
|
|
if(!message.token)
|
|
|
|
{
|
2011-08-17 16:28:30 +00:00
|
|
|
messageLogger.warn("Droped message, CLIENT_READY Message has no token!");
|
|
|
|
return;
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
if(!message.padId)
|
|
|
|
{
|
2011-08-17 16:28:30 +00:00
|
|
|
messageLogger.warn("Droped message, CLIENT_READY Message has no padId!");
|
|
|
|
return;
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
if(!message.protocolVersion)
|
|
|
|
{
|
2011-08-17 16:28:30 +00:00
|
|
|
messageLogger.warn("Droped message, CLIENT_READY Message has no protocolVersion!");
|
|
|
|
return;
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
2011-06-20 10:44:04 +00:00
|
|
|
if(message.protocolVersion != 2)
|
2011-03-26 13:10:41 +00:00
|
|
|
{
|
2011-08-17 16:28:30 +00:00
|
|
|
messageLogger.warn("Droped message, CLIENT_READY Message has a unkown protocolVersion '" + message.protocolVersion + "'!");
|
|
|
|
return;
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
|
2011-05-16 12:10:53 +00:00
|
|
|
var author;
|
|
|
|
var authorName;
|
|
|
|
var authorColorId;
|
2011-05-17 15:33:54 +00:00
|
|
|
var pad;
|
2011-06-30 11:03:20 +00:00
|
|
|
var historicalAuthorData = {};
|
2011-07-08 16:42:07 +00:00
|
|
|
var readOnlyId;
|
2011-07-14 15:15:38 +00:00
|
|
|
var chatMessages;
|
2011-05-16 12:10:53 +00:00
|
|
|
|
2011-05-17 15:33:54 +00:00
|
|
|
async.series([
|
2011-08-13 21:07:21 +00:00
|
|
|
//check permissions
|
2011-05-16 12:10:53 +00:00
|
|
|
function(callback)
|
2011-03-27 11:49:04 +00:00
|
|
|
{
|
2011-08-13 21:07:21 +00:00
|
|
|
securityManager.checkAccess (message.padId, message.sessionID, message.token, message.password, function(err, statusObject)
|
2011-03-27 11:49:04 +00:00
|
|
|
{
|
2011-08-13 21:07:21 +00:00
|
|
|
if(err) {callback(err); return}
|
2011-05-16 12:10:53 +00:00
|
|
|
|
2011-08-13 21:07:21 +00:00
|
|
|
//access was granted
|
|
|
|
if(statusObject.accessStatus == "grant")
|
|
|
|
{
|
|
|
|
author = statusObject.authorID;
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
//no access, send the client a message that tell him why
|
|
|
|
else
|
|
|
|
{
|
2011-08-15 17:26:20 +00:00
|
|
|
client.json.send({accessStatus: statusObject.accessStatus})
|
2011-08-13 21:07:21 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
//get all authordata of this new user
|
|
|
|
function(callback)
|
|
|
|
{
|
|
|
|
async.parallel([
|
|
|
|
//get colorId
|
|
|
|
function(callback)
|
|
|
|
{
|
|
|
|
authorManager.getAuthorColorId(author, function(err, value)
|
2011-05-16 12:10:53 +00:00
|
|
|
{
|
2011-08-13 21:07:21 +00:00
|
|
|
authorColorId = value;
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
//get author name
|
|
|
|
function(callback)
|
|
|
|
{
|
|
|
|
authorManager.getAuthorName(author, function(err, value)
|
2011-05-16 12:10:53 +00:00
|
|
|
{
|
2011-08-13 21:07:21 +00:00
|
|
|
authorName = value;
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
function(callback)
|
|
|
|
{
|
|
|
|
padManager.getPad(message.padId, function(err, value)
|
2011-05-17 15:33:54 +00:00
|
|
|
{
|
2011-08-13 21:07:21 +00:00
|
|
|
pad = value;
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
function(callback)
|
|
|
|
{
|
|
|
|
readOnlyManager.getReadOnlyId(message.padId, function(err, value)
|
2011-07-08 16:42:07 +00:00
|
|
|
{
|
2011-08-13 21:07:21 +00:00
|
|
|
readOnlyId = value;
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
], callback);
|
2011-03-26 13:10:41 +00:00
|
|
|
},
|
2011-07-14 15:15:38 +00:00
|
|
|
//these db requests all need the pad object
|
2011-05-16 12:10:53 +00:00
|
|
|
function(callback)
|
2011-06-30 11:03:20 +00:00
|
|
|
{
|
|
|
|
var authors = pad.getAllAuthors();
|
|
|
|
|
2011-07-14 15:15:38 +00:00
|
|
|
async.parallel([
|
|
|
|
//get all author data out of the database
|
|
|
|
function(callback)
|
2011-06-30 11:03:20 +00:00
|
|
|
{
|
2011-07-14 15:15:38 +00:00
|
|
|
async.forEach(authors, function(authorId, callback)
|
|
|
|
{
|
|
|
|
authorManager.getAuthor(authorId, function(err, author)
|
|
|
|
{
|
2011-07-31 15:16:59 +00:00
|
|
|
delete author.timestamp;
|
2011-07-14 15:15:38 +00:00
|
|
|
historicalAuthorData[authorId] = author;
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
}, callback);
|
|
|
|
},
|
|
|
|
//get the latest chat messages
|
|
|
|
function(callback)
|
|
|
|
{
|
2011-08-17 19:04:28 +00:00
|
|
|
pad.getLastChatMessages(100, function(err, _chatMessages)
|
2011-07-14 15:15:38 +00:00
|
|
|
{
|
|
|
|
chatMessages = _chatMessages;
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
], callback);
|
|
|
|
|
|
|
|
|
2011-06-30 11:03:20 +00:00
|
|
|
},
|
|
|
|
function(callback)
|
2011-05-16 12:10:53 +00:00
|
|
|
{
|
2011-06-04 17:02:22 +00:00
|
|
|
//Check if this author is already on the pad, if yes, kick the other sessions!
|
2011-05-16 12:10:53 +00:00
|
|
|
if(pad2sessions[message.padId])
|
|
|
|
{
|
|
|
|
for(var i in pad2sessions[message.padId])
|
|
|
|
{
|
|
|
|
if(sessioninfos[pad2sessions[message.padId][i]].author == author)
|
|
|
|
{
|
2011-08-16 19:02:30 +00:00
|
|
|
socketio.sockets.sockets[pad2sessions[message.padId][i]].json.send({disconnect:"userdup"});
|
2011-05-16 12:10:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//Save in session2pad that this session belonges to this pad
|
2011-06-23 13:08:18 +00:00
|
|
|
var sessionId=String(client.id);
|
2011-05-16 12:10:53 +00:00
|
|
|
session2pad[sessionId] = message.padId;
|
|
|
|
|
|
|
|
//check if there is already a pad2sessions entry, if not, create one
|
|
|
|
if(!pad2sessions[message.padId])
|
|
|
|
{
|
|
|
|
pad2sessions[message.padId] = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
//Saves in pad2sessions that this session belongs to this pad
|
|
|
|
pad2sessions[message.padId].push(sessionId);
|
|
|
|
|
|
|
|
//prepare all values for the wire
|
2011-05-18 22:04:11 +00:00
|
|
|
var atext = Changeset.cloneAText(pad.atext);
|
2011-05-16 15:28:58 +00:00
|
|
|
var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
|
2011-05-16 12:10:53 +00:00
|
|
|
var apool = attribsForWire.pool.toJsonable();
|
|
|
|
atext.attribs = attribsForWire.translated;
|
|
|
|
|
2011-08-21 19:09:25 +00:00
|
|
|
//check if abiword is avaiable
|
|
|
|
var abiwordAvailable = settings.abiword != null ? "yes" : "no";
|
|
|
|
if(settings.abiword != null && os.type().indexOf("Windows") != -1)
|
|
|
|
{
|
|
|
|
abiwordAvailable = "withoutPDF";
|
|
|
|
}
|
|
|
|
|
2011-05-16 12:10:53 +00:00
|
|
|
var clientVars = {
|
|
|
|
"accountPrivs": {
|
|
|
|
"maxRevisions": 100
|
|
|
|
},
|
|
|
|
"initialRevisionList": [],
|
|
|
|
"initialOptions": {
|
|
|
|
"guestPolicy": "deny"
|
|
|
|
},
|
|
|
|
"collab_client_vars": {
|
|
|
|
"initialAttributedText": atext,
|
2011-07-31 15:14:55 +00:00
|
|
|
"clientIp": "127.0.0.1",
|
2011-05-16 12:10:53 +00:00
|
|
|
//"clientAgent": "Anonymous Agent",
|
|
|
|
"padId": message.padId,
|
2011-06-30 11:03:20 +00:00
|
|
|
"historicalAuthorData": historicalAuthorData,
|
2011-05-16 12:10:53 +00:00
|
|
|
"apool": apool,
|
|
|
|
"rev": pad.getHeadRevisionNumber(),
|
|
|
|
"globalPadId": message.padId
|
|
|
|
},
|
2011-08-16 20:29:51 +00:00
|
|
|
"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"],
|
2011-07-31 15:14:55 +00:00
|
|
|
"clientIp": "127.0.0.1",
|
2011-05-16 12:10:53 +00:00
|
|
|
"userIsGuest": true,
|
|
|
|
"userColor": authorColorId,
|
2011-03-26 13:10:41 +00:00
|
|
|
"padId": message.padId,
|
2011-05-16 12:10:53 +00:00
|
|
|
"initialTitle": "Pad: " + message.padId,
|
|
|
|
"opts": {},
|
2011-07-14 15:15:38 +00:00
|
|
|
"chatHistory": chatMessages,
|
2011-05-16 12:10:53 +00:00
|
|
|
"numConnectedUsers": pad2sessions[message.padId].length,
|
|
|
|
"isProPad": false,
|
2011-07-08 16:42:07 +00:00
|
|
|
"readOnlyId": readOnlyId,
|
2011-05-16 12:10:53 +00:00
|
|
|
"serverTimestamp": new Date().getTime(),
|
|
|
|
"globalPadId": message.padId,
|
|
|
|
"userId": author,
|
|
|
|
"cookiePrefsToSet": {
|
|
|
|
"fullWidth": false,
|
|
|
|
"hideSidebar": false
|
|
|
|
},
|
2011-08-21 19:09:25 +00:00
|
|
|
"abiwordAvailable": abiwordAvailable,
|
2011-05-16 12:10:53 +00:00
|
|
|
"hooks": {}
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
2011-05-16 12:10:53 +00:00
|
|
|
|
|
|
|
//Add a username to the clientVars if one avaiable
|
|
|
|
if(authorName != null)
|
|
|
|
{
|
|
|
|
clientVars.userName = authorName;
|
|
|
|
}
|
|
|
|
|
|
|
|
//Send the clientVars to the Client
|
2011-06-23 13:08:18 +00:00
|
|
|
client.json.send(clientVars);
|
2011-05-16 12:10:53 +00:00
|
|
|
|
2011-10-19 19:48:36 +00:00
|
|
|
//sometimes the client disconnects very early and the session of it is already removed
|
|
|
|
//thats why we have to check that case
|
|
|
|
if(sessioninfos[client.id] !== undefined)
|
|
|
|
{
|
|
|
|
//Save the revision and the author id in sessioninfos
|
|
|
|
sessioninfos[client.id].rev = pad.getHeadRevisionNumber();
|
|
|
|
sessioninfos[client.id].author = author;
|
|
|
|
}
|
|
|
|
|
2011-05-16 12:10:53 +00:00
|
|
|
//prepare the notification for the other users on the pad, that this user joined
|
|
|
|
var messageToTheOtherUsers = {
|
2011-03-26 13:10:41 +00:00
|
|
|
"type": "COLLABROOM",
|
|
|
|
"data": {
|
|
|
|
type: "USER_NEWINFO",
|
|
|
|
userInfo: {
|
|
|
|
"ip": "127.0.0.1",
|
2011-05-16 12:10:53 +00:00
|
|
|
"colorId": authorColorId,
|
2011-03-26 13:10:41 +00:00
|
|
|
"userAgent": "Anonymous",
|
2011-05-16 12:10:53 +00:00
|
|
|
"userId": author
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2011-05-16 12:10:53 +00:00
|
|
|
|
|
|
|
//Add the authorname of this new User, if avaiable
|
|
|
|
if(authorName != null)
|
|
|
|
{
|
|
|
|
messageToTheOtherUsers.data.userInfo.name = authorName;
|
|
|
|
}
|
|
|
|
|
|
|
|
//Run trough all sessions of this pad
|
|
|
|
async.forEach(pad2sessions[message.padId], function(sessionID, callback)
|
|
|
|
{
|
|
|
|
var sessionAuthorName, sessionAuthorColorId;
|
|
|
|
|
|
|
|
async.series([
|
|
|
|
//get the authorname & colorId
|
|
|
|
function(callback)
|
|
|
|
{
|
|
|
|
async.parallel([
|
|
|
|
function(callback)
|
|
|
|
{
|
|
|
|
authorManager.getAuthorColorId(sessioninfos[sessionID].author, function(err, value)
|
|
|
|
{
|
|
|
|
sessionAuthorColorId = value;
|
|
|
|
callback(err);
|
|
|
|
})
|
|
|
|
},
|
|
|
|
function(callback)
|
|
|
|
{
|
|
|
|
authorManager.getAuthorName(sessioninfos[sessionID].author, function(err, value)
|
|
|
|
{
|
|
|
|
sessionAuthorName = value;
|
|
|
|
callback(err);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
],callback);
|
|
|
|
},
|
|
|
|
function (callback)
|
|
|
|
{
|
|
|
|
//Jump over, if this session is the connection session
|
2011-06-23 13:08:18 +00:00
|
|
|
if(sessionID != client.id)
|
2011-05-16 12:10:53 +00:00
|
|
|
{
|
|
|
|
//Send this Session the Notification about the new user
|
2011-07-05 15:07:09 +00:00
|
|
|
socketio.sockets.sockets[sessionID].json.send(messageToTheOtherUsers);
|
2011-05-16 12:10:53 +00:00
|
|
|
|
|
|
|
//Send the new User a Notification about this other user
|
|
|
|
var messageToNotifyTheClientAboutTheOthers = {
|
|
|
|
"type": "COLLABROOM",
|
|
|
|
"data": {
|
|
|
|
type: "USER_NEWINFO",
|
|
|
|
userInfo: {
|
|
|
|
"ip": "127.0.0.1",
|
|
|
|
"colorId": sessionAuthorColorId,
|
|
|
|
"name": sessionAuthorName,
|
|
|
|
"userAgent": "Anonymous",
|
|
|
|
"userId": sessioninfos[sessionID].author
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2011-06-23 13:08:18 +00:00
|
|
|
client.json.send(messageToNotifyTheClientAboutTheOthers);
|
2011-05-16 12:10:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
], callback);
|
|
|
|
}, callback);
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|
2011-05-16 12:10:53 +00:00
|
|
|
],function(err)
|
|
|
|
{
|
|
|
|
if(err) throw err;
|
|
|
|
});
|
2011-03-26 13:10:41 +00:00
|
|
|
}
|