// vim:set noexpandtab: /************** SYSTEM INCLUDES **************/ var http = require("http"); var reload = require("reload"); var sanitizer = require("sanitizer"); var compression = require("compression"); var express = require("express"); var conf = require("./config.js").server; var headerBarUrl = require("./config.js").headerBarUrl; var logoUrl = require("./config.js").logoUrl; /************** LOCAL INCLUDES **************/ var rooms = require("./lib/rooms.js"); var data = require("./lib/data.js").db; /************** GLOBALS **************/ //Map of sids to user_names var sids_to_user_names = []; /************** SETUP EXPRESS **************/ var app = express(); var router = express.Router(); app.use(compression()); app.use(conf.baseurl, router); router.use(express.static(__dirname + "/node_modules")); router.use(express.static(__dirname + "/client")); var server = http.createServer(app); // Reload code here reload(app) .then(function (reloadReturned) { // reloadReturned is documented in the returns API in the README // Reload started, start web server server.listen(conf.port, function () { console.log("Web server available on http://127.0.0.1:" + conf.port); }); }) .catch(function (err) { console.error( "Reload could not start, could not start server/sample app", err ); }); /************** SETUP Socket.IO **************/ var io = require("socket.io")(server, { path: conf.baseurl == "/" ? "" : conf.baseurl + "/socket.io", cookie: false }); /************** ROUTES **************/ router.get("/", function (req, res) { //console.log(req.header('host')); url = req.header("host") + req.baseUrl; var connected = io.sockets.connected; clientsCount = Object.keys(connected).length; res.render("home.jade", { url: url, headerBarUrl: headerBarUrl, logoUrl: logoUrl, connected: clientsCount, home: true, }); }); router.get("/demo", function (req, res) { url = req.header("host") + req.baseUrl; res.render("index.jade", { pageTitle: "Post-it - demo", headerBarUrl: headerBarUrl, logoUrl: logoUrl, url: url, demo: true, }); }); router.get("/:id", function (req, res) { url = req.header("host") + req.baseUrl; res.render("index.jade", { pageTitle: "Post-it - " + req.params.id, headerBarUrl: headerBarUrl, logoUrl: logoUrl, url: url, }); }); router.get("/stats", function (req, res) { console.log("TODO: stats"); }); /************** SOCKET.I0 **************/ //sanitizes text function scrub(text) { if (typeof text != "undefined" && text !== null) { //clip the string if it is too long if (text.length > 65535) { text = text.substr(0, 65535); } return sanitizer.sanitize(text); } else { return null; } } io.sockets.on("connection", function (client) { client.on("message", function (message) { //console.log(message.action + " -- " + sys.inspect(message.data) ); var clean_data = {}; var clean_message = {}; var message_out = {}; if (!message.action) return; switch (message.action) { case "initializeMe": initClient(client); break; case "joinRoom": joinRoom(client, message.data, function (clients) { client.json.send({ action: "roomAccept", data: "" }); }); break; case "moveCard": //report to all other browsers message_out = { action: message.action, data: { id: scrub(message.data.id), position: { left: scrub(message.data.position.left), top: scrub(message.data.position.top), }, }, }; broadcastToRoom(client, message_out); // console.log("-----" + message.data.id); // console.log(JSON.stringify(message.data)); getRoom(client, function (room) { db.cardSetXY( room, message.data.id, message.data.position.left, message.data.position.top ); }); break; case "createCard": data = message.data; clean_data = {}; clean_data.text = scrub(data.text); clean_data.id = scrub(data.id); clean_data.x = scrub(data.x); clean_data.y = scrub(data.y); clean_data.rot = scrub(data.rot); clean_data.colour = scrub(data.colour); getRoom(client, function (room) { createCard( room, clean_data.id, clean_data.text, clean_data.x, clean_data.y, clean_data.rot, clean_data.colour ); }); message_out = { action: "createCard", data: clean_data, }; //report to all other browsers broadcastToRoom(client, message_out); break; case "editCard": clean_data = {}; clean_data.value = scrub(message.data.value); clean_data.id = scrub(message.data.id); //send update to database getRoom(client, function (room) { db.cardEdit(room, clean_data.id, clean_data.value); }); message_out = { action: "editCard", data: clean_data, }; broadcastToRoom(client, message_out); break; case "deleteCard": clean_message = { action: "deleteCard", data: { id: scrub(message.data.id) }, }; getRoom(client, function (room) { db.deleteCard(room, clean_message.data.id); }); //report to all other browsers broadcastToRoom(client, clean_message); break; case "createColumn": clean_message = { data: scrub(message.data) }; getRoom(client, function (room) { db.createColumn(room, clean_message.data, function () {}); }); broadcastToRoom(client, clean_message); break; case "deleteColumn": getRoom(client, function (room) { db.deleteColumn(room); }); broadcastToRoom(client, { action: "deleteColumn" }); break; case "updateColumns": var columns = message.data; if (!(columns instanceof Array)) break; var clean_columns = []; for (var i in columns) { clean_columns[i] = scrub(columns[i]); } getRoom(client, function (room) { db.setColumns(room, clean_columns); }); broadcastToRoom(client, { action: "updateColumns", data: clean_columns, }); break; case "changeTheme": clean_message = {}; clean_message.data = scrub(message.data); getRoom(client, function (room) { db.setTheme(room, clean_message.data); }); clean_message.action = "changeTheme"; broadcastToRoom(client, clean_message); break; case "setUserName": clean_message = {}; clean_message.data = scrub(message.data); setUserName(client, clean_message.data); var msg = {}; msg.action = "nameChangeAnnounce"; msg.data = { sid: client.id, user_name: clean_message.data }; broadcastToRoom(client, msg); break; case "addSticker": var cardId = scrub(message.data.cardId); var stickerId = scrub(message.data.stickerId); getRoom(client, function (room) { db.addSticker(room, cardId, stickerId); }); broadcastToRoom(client, { action: "addSticker", data: { cardId: cardId, stickerId: stickerId }, }); break; case "setBoardSize": var size = {}; size.width = scrub(message.data.width); size.height = scrub(message.data.height); getRoom(client, function (room) { db.setBoardSize(room, size); }); broadcastToRoom(client, { action: "setBoardSize", data: size }); break; case "exportTxt": exportBoard("txt", client, message.data); break; case "exportCsv": exportBoard("csv", client, message.data); break; case "exportJson": exportJson(client, message.data); break; case "importJson": importJson(client, message.data); break; case "createRevision": createRevision(client, message.data); break; case "deleteRevision": deleteRevision(client, message.data); break; case "exportRevision": exportRevision(client, message.data); break; default: //console.log('unknown action'); break; } }); client.on("disconnect", function () { leaveRoom(client); }); //tell all others that someone has connected //client.broadcast('someone has connected'); }); /************** FUNCTIONS **************/ function initClient(client) { //console.log ('initClient Started'); getRoom(client, function (room) { db.getAllCards(room, function (cards) { client.json.send({ action: "initCards", data: cards, }); }); db.getAllColumns(room, function (columns) { client.json.send({ action: "initColumns", data: columns, }); }); db.getRevisions(room, function (revisions) { client.json.send({ action: "initRevisions", data: revisions !== null ? Object.keys(revisions) : new Array(), }); }); db.getTheme(room, function (theme) { if (theme === null) theme = "bigcards"; client.json.send({ action: "changeTheme", data: theme, }); }); db.getBoardSize(room, function (size) { if (size !== null) { client.json.send({ action: "setBoardSize", data: size, }); } }); roommates_clients = rooms.room_clients(room); roommates = []; var j = 0; for (var i in roommates_clients) { if (roommates_clients[i].id != client.id) { roommates[j] = { sid: roommates_clients[i].id, user_name: sids_to_user_names[roommates_clients[i].id], }; j++; } } //console.log('initialusers: ' + roommates); client.json.send({ action: "initialUsers", data: roommates, }); }); } function joinRoom(client, room, successFunction) { var msg = {}; msg.action = "join-announce"; msg.data = { sid: client.id, user_name: client.user_name }; rooms.add_to_room_and_announce(client, room, msg); successFunction(); } function leaveRoom(client) { //console.log (client.id + ' just left'); var msg = {}; msg.action = "leave-announce"; msg.data = { sid: client.id }; rooms.remove_from_all_rooms_and_announce(client, msg); delete sids_to_user_names[client.id]; } function broadcastToRoom(client, message) { rooms.broadcast_to_roommates(client, message); } //----------------CARD FUNCTIONS function createCard(room, id, text, x, y, rot, colour) { var card = { id: id, colour: colour, rot: rot, x: x, y: y, text: text, sticker: null, }; db.createCard(room, id, card); } function roundRand(max) { return Math.floor(Math.random() * max); } //------------ROOM STUFF // Get Room name for the given Session ID function getRoom(client, callback) { room = rooms.get_room(client); //console.log( 'client: ' + client.id + " is in " + room); callback(room); } function setUserName(client, name) { client.user_name = name; sids_to_user_names[client.id] = name; //console.log('sids to user names: '); console.dir(sids_to_user_names); } function cleanAndInitializeDemoRoom() { // DUMMY DATA db.clearRoom("/demo", function () { db.createColumn("/demo", "Pas commencé"); db.createColumn("/demo", "Commencé"); db.createColumn("/demo", "En test"); db.createColumn("/demo", "Validation"); db.createColumn("/demo", "Terminé"); createCard( "/demo", "card1", "Salut, c'est fun", roundRand(600), roundRand(300), Math.random() * 10 - 5, "yellow" ); createCard( "/demo", "card2", "Salut, c'est une nouvelle histoire.", roundRand(600), roundRand(300), Math.random() * 10 - 5, "white" ); createCard( "/demo", "card3", ".", roundRand(600), roundRand(300), Math.random() * 10 - 5, "blue" ); createCard( "/demo", "card4", ".", roundRand(600), roundRand(300), Math.random() * 10 - 5, "green" ); createCard( "/demo", "card5", "Salut, c'est fun", roundRand(600), roundRand(300), Math.random() * 10 - 5, "yellow" ); createCard( "/demo", "card6", "Salut, c'est un nouveau mémo.", roundRand(600), roundRand(300), Math.random() * 10 - 5, "yellow" ); createCard( "/demo", "card7", ".", roundRand(600), roundRand(300), Math.random() * 10 - 5, "blue" ); createCard( "/demo", "card8", ".", roundRand(600), roundRand(300), Math.random() * 10 - 5, "green" ); }); } // Export board in txt or csv function exportBoard(format, client, data) { var result = new Array(); getRoom(client, function (room) { db.getAllCards(room, function (cards) { db.getAllColumns(room, function (columns) { var text = new Array(); var cols = {}; if (columns.length > 0) { for (var i = 0; i < columns.length; i++) { cols[columns[i]] = new Array(); for (var j = 0; j < cards.length; j++) { if (i === 0) { if (cards[j]["x"] < (i + 1) * data) { cols[columns[i]].push(cards[j]); } } else if (i + 1 === columns.length) { if (cards[j]["x"] >= i * data) { cols[columns[i]].push(cards[j]); } } else if ( cards[j]["x"] >= i * data && cards[j]["x"] < (i + 1) * data ) { cols[columns[i]].push(cards[j]); } } cols[columns[i]].sort(function (a, b) { if (a["y"] === b["y"]) { return a["x"] - b["x"]; } else { return a["y"] - b["y"]; } }); } if (format === "txt") { for (var i = 0; i < columns.length; i++) { if (i === 0) { text.push("# " + columns[i]); } else { text.push("\n# " + columns[i]); } for (var j = 0; j < cols[columns[i]].length; j++) { text.push("- " + cols[columns[i]][j]["text"]); } } } else if (format === "csv") { var max = 0; var line = new Array(); var patt_vuln = new RegExp("^[=+-@]"); for (var i = 0; i < columns.length; i++) { if (cols[columns[i]].length > max) { max = cols[columns[i]].length; } var val = columns[i].replace(/"/g, '""'); if (patt_vuln.test(val)) { // prevent CSV Formula Injection var val = "'" + val; } line.push('"' + val + '"'); } text.push(line.join(",")); for (var j = 0; j < max; j++) { line = new Array(); for (var i = 0; i < columns.length; i++) { var val = cols[columns[i]][j] !== undefined ? cols[columns[i]][j]["text"].replace(/"/g, '""') : ""; if (patt_vuln.test(val)) { // prevent CSV Formula Injection var val = "'" + val; } line.push('"' + val + '"'); } text.push(line.join(",")); } } } else { for (var j = 0; j < cards.length; j++) { if (format === "txt") { text.push("- " + cards[j]["text"]); } else if (format === "csv") { text.push('"' + cards[j]["text"].replace(/"/g, '""') + '"\n'); } } } var result; if (format === "txt" || format === "csv") { result = text.join("\n"); } else if (format === "json") { result = JSON.stringify(cols); } client.json.send({ action: "export", data: { filename: room.replace("/", "") + "." + format, text: result, }, }); }); }); }); } // Export board in json, suitable for import function exportJson(client, data) { var result = new Array(); getRoom(client, function (room) { db.getAllCards(room, function (cards) { db.getAllColumns(room, function (columns) { db.getTheme(room, function (theme) { db.getBoardSize(room, function (size) { if (theme === null) theme = "bigcards"; if (size === null) size = { width: data.width, height: data.height }; result = JSON.stringify({ cards: cards, columns: columns, theme: theme, size: size, }); client.json.send({ action: "export", data: { filename: room.replace("/", "") + ".json", text: result, }, }); }); }); }); }); }); } // Import board from json function importJson(client, data) { getRoom(client, function (room) { db.clearRoom(room, function () { db.getAllCards(room, function (cards) { for (var i = 0; i < cards.length; i++) { db.deleteCard(room, cards[i].id); } cards = data.cards; var cards2 = new Array(); for (var i = 0; i < cards.length; i++) { var card = cards[i]; if ( card.id !== undefined && card.colour !== undefined && card.rot !== undefined && card.x !== undefined && card.y !== undefined && card.text !== undefined && card.sticker !== undefined ) { var c = { id: card.id, colour: card.colour, rot: card.rot, x: card.x, y: card.y, text: scrub(card.text), sticker: card.sticker, }; db.createCard(room, c.id, c); cards2.push(c); } } var msg = { action: "initCards", data: cards2 }; broadcastToRoom(client, msg); client.json.send(msg); }); db.getAllColumns(room, function (columns) { for (var i = 0; i < columns.length; i++) { db.deleteColumn(room); } columns = data.columns; var columns2 = new Array(); for (var i = 0; i < columns.length; i++) { var column = scrub(columns[i]); if (typeof column === "string") { db.createColumn(room, column); columns2.push(column); } } msg = { action: "initColumns", data: columns2 }; broadcastToRoom(client, msg); client.json.send(msg); }); var size = data.size; if (size.width !== undefined && size.height !== undefined) { size = { width: scrub(size.width), height: scrub(size.height) }; db.setBoardSize(room, size); msg = { action: "setBoardSize", data: size }; broadcastToRoom(client, msg); client.json.send(msg); } data.theme = scrub(data.theme); if (data.theme === "smallcards" || data.theme === "bigcards") { db.setTheme(room, data.theme); msg = { action: "changeTheme", data: data.theme }; broadcastToRoom(client, msg); client.json.send(msg); } }); }); } // function createRevision(client, data) { var result = new Array(); getRoom(client, function (room) { db.getAllCards(room, function (cards) { db.getAllColumns(room, function (columns) { db.getTheme(room, function (theme) { db.getBoardSize(room, function (size) { if (theme === null) theme = "bigcards"; if (size === null) size = { width: data.width, height: data.height }; result = { cards: cards, columns: columns, theme: theme, size: size, }; var timestamp = Date.now(); db.getRevisions(room, function (revisions) { if (revisions === null) revisions = {}; revisions[timestamp + ""] = result; db.setRevisions(room, revisions); msg = { action: "addRevision", data: timestamp }; broadcastToRoom(client, msg); client.json.send(msg); }); }); }); }); }); }); } function deleteRevision(client, timestamp) { getRoom(client, function (room) { db.getRevisions(room, function (revisions) { if (revisions !== null && revisions[timestamp + ""] !== undefined) { delete revisions[timestamp + ""]; db.setRevisions(room, revisions); } msg = { action: "deleteRevision", data: timestamp }; broadcastToRoom(client, msg); client.json.send(msg); }); }); } function exportRevision(client, timestamp) { getRoom(client, function (room) { db.getRevisions(room, function (revisions) { if (revisions !== null && revisions[timestamp + ""] !== undefined) { client.json.send({ action: "export", data: { filename: room.replace("/", "") + "-" + timestamp + ".json", text: JSON.stringify(revisions[timestamp + ""]), }, }); } else { client.json.send({ action: "message", data: "Unable to find revision " + timestamp + ".", }); } }); }); } /************** SETUP DATABASE ON FIRST RUN **************/ // (runs only once on startup) var db = new data(function () { cleanAndInitializeDemoRoom(); });