const http = require('http') const express = require('express') const compression = require('compression') const sanitizer = require('sanitizer') const socketIO = require('socket.io') const reload = require('reload') const conf = require('./config.js').server const { headerBarUrl, logoUrl } = require('./config.js') const app = express() const router = express.Router() app.use(compression()) app.use(conf.baseurl, router) router.use(express.static(`${__dirname}/node_modules`)) router.use(express.static(`${__dirname}/client`)) const server = http.createServer(app) const io = socketIO(server, { // Use the renamed socketIO require path: conf.baseurl === '/' ? '' : `${conf.baseurl}/socket.io`, cookie: false }) /** ************ LOCAL INCLUDES ************* */ const rooms = require('./lib/rooms.js') let data = require('./lib/data.js').db /** ************ GLOBALS ************* */ // Map of sids to user_names const sids_to_user_names = [] // Reload code here reload(app) .then((reloadReturned) => { // reloadReturned is documented in the returns API in the README // Reload started, start web server server.listen(conf.port, () => { console.log(`Web server available on http://127.0.0.1:${conf.port}`) }) }) .catch((err) => { console.error( 'Reload could not start, could not start server/sample app', err ) }) /** ************ SETUP Socket.IO ************* */ /** ************ ROUTES ************* */ router.get('/', (req, res) => { // console.log(req.header('host')); url = req.header('host') + req.baseUrl const { connected } = io.sockets clientsCount = Object.keys(connected).length res.render('home.jade', { url, headerBarUrl, logoUrl, connected: clientsCount, home: true }) }) router.get('/demo', (req, res) => { url = req.header('host') + req.baseUrl res.render('index.jade', { pageTitle: 'Memo - demo', headerBarUrl, logoUrl, url, demo: true }) }) router.get('/:id', (req, res) => { url = req.header('host') + req.baseUrl res.render('index.jade', { pageTitle: `Memo - ${req.params.id}`, headerBarUrl, logoUrl, url }) }) router.get('/stats', (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) } return null } io.sockets.on('connection', (client) => { client.on('message', (message) => { // console.log(message.action + " -- " + sys.inspect(message.data) ); let clean_data = {} let clean_message = {} let message_out = {} if (!message.action) return switch (message.action) { case 'initializeMe': initClient(client) break case 'joinRoom': joinRoom(client, message.data, (clients) => { client.json.send({ action: 'roomAccept', data: '' }) }) break case 'editBoardMetas': const id = scrub(message.data.id) const prop = scrub(message.data.prop) const value = scrub(message.data.value) const clean_data = { id, prop, value } getRoom(client, (room) => { const boardMeta = { prop, value } // Enregistre les metas dans Redis db.createBoardMetas(room, id, boardMeta) }) const message_out = { action: 'editBoardMetas', data: clean_data } broadcastToRoom(client, message_out) 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, (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, (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, (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, (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, (room) => { db.createColumn(room, clean_message.data, () => {}) }) broadcastToRoom(client, clean_message) break case 'deleteColumn': getRoom(client, (room) => { db.deleteColumn(room) }) broadcastToRoom(client, { action: 'deleteColumn' }) break case 'updateColumns': var columns = message.data if (!(columns instanceof Array)) break var clean_columns = [] for (const i in columns) { clean_columns[i] = scrub(columns[i]) } getRoom(client, (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, (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, (room) => { db.addSticker(room, cardId, stickerId) }) broadcastToRoom(client, { action: 'addSticker', data: { cardId, stickerId } }) break case 'setBoardSize': var size = {} size.width = scrub(message.data.width) size.height = scrub(message.data.height) getRoom(client, (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', () => { leaveRoom(client) }) // tell all others that someone has connected // client.broadcast('someone has connected'); }) /** ************ FUNCTIONS ************* */ function initClient(client) { // console.log ('initClient Started'); getRoom(client, (room) => { db.getAllCards(room, (cards) => { client.json.send({ action: 'initCards', data: cards }) }) db.getAllColumns(room, (columns) => { client.json.send({ action: 'initColumns', data: columns }) }) db.getRevisions(room, (revisions) => { client.json.send({ action: 'initRevisions', data: revisions !== null ? Object.keys(revisions) : [] }) }) db.getTheme(room, (theme) => { if (theme === null) theme = 'bigcards' client.json.send({ action: 'changeTheme', data: theme }) }) db.getBoardSize(room, (size) => { if (size !== null) { client.json.send({ action: 'setBoardSize', data: size }) } }) db.getBoardMetas(room, (metas) => { if (metas) { for (const id in metas) { const meta = metas[id] client.json.send({ action: 'editBoardMetas', data: { id, prop: meta.prop, value: meta.value } }) } } }) roommates_clients = rooms.room_clients(room) roommates = [] let j = 0 for (const 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) { const 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'); const 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) { const card = { id, colour, rot, x, y, 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', () => { 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) { const result = [] getRoom(client, (room) => { db.getAllCards(room, (cards) => { db.getAllColumns(room, (columns) => { const text = [] const cols = {} if (columns.length > 0) { for (var i = 0; i < columns.length; i++) { cols[columns[i]] = [] 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((a, b) => { if (a.y === b.y) { return a.x - b.x } 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') { let max = 0 let line = [] const 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 = [] 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`) } } } let 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) { let result = [] getRoom(client, (room) => { db.getAllCards(room, (cards) => { db.getAllColumns(room, (columns) => { db.getTheme(room, (theme) => { db.getBoardSize(room, (size) => { if (theme === null) theme = 'bigcards' if (size === null) { size = { width: data.width, height: data.height } } result = JSON.stringify({ cards, columns, theme, size }) client.json.send({ action: 'export', data: { filename: `${room.replace('/', '')}.json`, text: result } }) }) }) }) }) }) } // Import board from json function importJson(client, data) { getRoom(client, (room) => { db.clearRoom(room, () => { db.getAllCards(room, (cards) => { for (var i = 0; i < cards.length; i++) { db.deleteCard(room, cards[i].id) } cards = data.cards const cards2 = [] for (var i = 0; i < cards.length; i++) { const 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 ) { const 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) } } const msg = { action: 'initCards', data: cards2 } broadcastToRoom(client, msg) client.json.send(msg) }) db.getAllColumns(room, (columns) => { for (var i = 0; i < columns.length; i++) { db.deleteColumn(room) } columns = data.columns const columns2 = [] for (var i = 0; i < columns.length; i++) { const 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) }) let { size } = data 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) { let result = [] getRoom(client, (room) => { db.getAllCards(room, (cards) => { db.getAllColumns(room, (columns) => { db.getTheme(room, (theme) => { db.getBoardSize(room, (size) => { if (theme === null) theme = 'bigcards' if (size === null) { size = { width: data.width, height: data.height } } result = { cards, columns, theme, size } const timestamp = Date.now() db.getRevisions(room, (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, (room) => { db.getRevisions(room, (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, (room) => { db.getRevisions(room, (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(() => { cleanAndInitializeDemoRoom() })