ripple-core
Version:
Ripple is an interactive audience response system that allows presenters to survey audience members in real time communication through their mobile devices.
674 lines (577 loc) • 22.3 kB
JavaScript
// Start App
console.log("==============================================");
console.log('Starting Ripple...');
/**
* Module dependencies.
*/
// Make sure CONFIG is loaded first, since the loader will auto-generate it if it doesn't exist
var CONFIG = require('./lib/config-loader.js');
// We have to force the DB connection to happen first - our application
// is completely reliant on having a DB connection right from the start.
var DB = require('./lib/db-manager.js')
, logger = require('./lib/log');
DB.connect(run);
// Default to production mode
process.env.NODE_ENV = process.env.NODE_ENV || "production";
// This is intentionally NOT indented in order to make it clear this code is the main app and
// not just some random function. This is created to satisfy the need for our DB to be ready
// when the app starts up while still dealing with the JS asynchronous nuances.
function run() {
console.log('Environment :: ', process.env.NODE_ENV );
console.log('DB :: ', 'monogodb://' + CONFIG.SERVER('DB_HOST') + "/" + CONFIG.SERVER('DB_NAME') + ":" + CONFIG.SERVER('DB_PORT') );
console.log("==============================================");
var express = require('express')
, MongoStore = require('connect-mongo')(express)
, partials = require('express-partials')
, http = require('http')
, path = require('path')
, fs = require("fs")
, _ = require('underscore')
, nowjs = require('now')
, util = require("util")
, combo = require('combohandler')
, comboError = require('./lib/combo-middleware').error
, plugin = require('./lib/plugins')
, logger = require('./lib/log')
, h5bp = require('./lib/h5bp-middleware')
, systemVariables = require('./lib/variables-middleware')
, log = logger.logPair
, DB = require('./lib/db-manager.js')
, SSM = require('./lib/session-manager.js')
, menuRights = require('./lib/menu-rights-middleware.js')
, qTypes = require('./lib/question-type-middleware.js')
, sessionAPI = require('./lib/nowjs-to-session.js')
, lessCompiler = require('./script/lessCompiler.js')
, sanitize = require('validator').sanitize;
/**
* Routing
*/
// Read in Routes
var routes = {}
, routeDir = "./routes/";
fs.readdirSync(routeDir).forEach(function(filename) {
if (filename.match(/\.js$/)) {
var route = {};
_.extend(route, module.require(routeDir + filename));
routes[filename.replace(".js", "")] = route;
}
});
logger.debugPair('Routes', util.inspect(routes));
/**
* App Information & Configurations
*/
// These are folders that will be publically accessible
var app = module.exports = express();
var publicDir = path.join(__dirname, 'public');
var customDir = path.join(__dirname, 'custom');
var pluginDir = path.join(__dirname, 'plugins');
// Set up the session store, passing in the various database options we have
var sessionStore = new MongoStore({
db: CONFIG.SERVER("DB_NAME"),
username: CONFIG.SERVER("DB_AUTH_NAME"),
password: CONFIG.SERVER("DB_AUTH_PASS"),
collection: "web_sessions",
clear_interval: 3600
})
app.configure(function(){
// These are functions and properites that are global to the templating system
// through the use of their locals object
app.use(function(req, res, next) {
require("./lib/view-helpers.js").addLocals(req, res);
next();
});
// Ripple loads on port 3000 and 4000 for ssl if not defined
app.set('www_port', process.env.PORT || CONFIG.SERVER('WWW_PORT') || 3000);
app.set('ssl_port', process.env.SSL_PORT || CONFIG.SERVER('SSL_PORT') || 4000);
app.set('views', __dirname + '/views');
// Ripple use the ejs templating system
// @url http://embeddedjs.com/
app.set('view engine', 'ejs');
app.set("view options", { layout: "layout.ejs" });
// Ripple also uses express partials for better reusability
app.use( partials() );
// Ripple uses html5 boilerplate code to modify the header so IE is set to edge
// and to remove the powered by header info
app.use( h5bp.ieEdgeChromeFrameHeader() );
app.use( h5bp.removePoweredBy() );
app.use( express.favicon() );
app.use( express.logger('dev') );
app.use( express.bodyParser() );
app.use( express.methodOverride() );
app.use( express.cookieParser( CONFIG.SERVER('SECRET_KEY')) );
app.use( express.session({ secret : CONFIG.SERVER('SECRET_KEY'), store : sessionStore, cookie: {maxAge: 24 * 60 * 60 * 1000} }) );
app.use( app.router );
// Set up the static routes for publicly accessible folders
app.use( '/', express.static(publicDir) );
app.use( '/custom', express.static(customDir, 'custom') )
app.use( '/plugins', express.static(pluginDir, 'plugins') )
app.use( comboError );
app.use( error404 );
});
// Setup default error routes
function error404(req, res, next) {
var errorMsg = "Page Not Found"
, locals = {
title: errorMsg,
error: "404 - " + errorMsg
}
res.status(404).render('404', locals);
};
var lessOptions = {
"baseDir": __dirname
};
// Development only configurations of app
app.configure('development', function(){
app.use( express.errorHandler({ dumpExceptions: true, showStack: true }) );
});
if( process.env.NODE_ENV === 'development'){
lessOptions["watch"] = true;
}
// Production only configurations of app
app.configure('production', function(){
app.use(express.errorHandler());
});
if( process.env.NODE_ENV === 'production'){
lessOptions["watch"] = false;
}
// Compile LESS to CSS
lessCompiler.init(lessOptions);
// Admin Middleware Functions
var adminMiddleware = [menuRights, systemVariables.load];
/**
* URIs in use
*/
/* Landing Area URLs */
app.get('/', routes.main.index);
app.post('/', routes.main.indexPost);
app.get('/signup', routes.main.signup);
app.post('/signup', routes.main.signupPost);
app.get('/logout', routes.main.logout);
app.post('/logout', routes.main.logoutPost);
app.get('/reset-password/:guid', routes.main.resetPwd);
app.post('/reset-password/:guid', routes.main.resetPwdPost);
/* Admin URLs */
app.get('/admin', adminMiddleware, routes.admin.dashboard);
app.get('/admin/session', adminMiddleware, qTypes.load, routes.admin.session);
app.get('/admin/session/close', routes.admin.sessionClose);
app.get('/admin/session/:setID', adminMiddleware, qTypes.load, routes.admin.session);
app.get('/admin/set/start', adminMiddleware, routes.admin.setStart);
app.post('/admin/set/start', routes.admin.setStartPost);
app.get('/admin/set/list', adminMiddleware, routes.admin.setList);
app.get('/admin/set/edit/:setID', adminMiddleware, qTypes.load, routes.admin.setEdit);
app.post('/admin/set/edit/:setID', routes.admin.setEditPost);
app.get('/admin/plugins', adminMiddleware, routes.admin.pluginList);
app.post('/admin/plugins', routes.admin.pluginListPost);
app.get('/admin/plugin/:pluginName', adminMiddleware, routes.admin.pluginConfig);
app.post('/admin/plugin/:pluginName', routes.admin.pluginConfigPost);
app.get('/admin/profile', adminMiddleware, routes.admin.profile);
app.post('/admin/profile', routes.admin.profileUpdate);
app.get('/admin/reports', adminMiddleware, routes.admin.reportList);
app.post('/admin/reports', adminMiddleware, routes.admin.reportListPost);
app.get('/admin/report/:sessionID', adminMiddleware, qTypes.load, routes.admin.reportItem);
app.get('/admin/report/:sessionID/csv', adminMiddleware, routes.admin.reportItemCSV);
app.get('/admin/permissions', adminMiddleware, routes.admin.permissions);
app.post('/admin/permissions', routes.admin.permissionsPost);
app.get('/admin/people', adminMiddleware, routes.admin.people);
app.post('/admin/people', routes.admin.peoplePost);
app.get('/admin/settings', adminMiddleware, routes.admin.settings);
app.post('/admin/settings', routes.admin.settingsPost);
/* Room URL */
app.get('/room/:id', qTypes.load, routes.client.index);
// Special Route for combining Client Side JS & CSS
app.get('/static', combo.combine({rootPath: publicDir}), function (req, res) {
res.send(res.body);
});
app.get('/static/js', combo.combine({rootPath: publicDir + '/js'}), function (req, res) {
res.send(res.body);
});
app.get('/static/plugins', combo.combine({rootPath: pluginDir}), function (req, res) {
res.send(res.body);
});
app.get('/static/custom', combo.combine({rootPath: customDir}), function (req, res) {
res.send(res.body);
});
app.get('/static/css', combo.combine({rootPath: publicDir + '/css'}), function (req, res) {
res.send(res.body);
});
/**
* HTTP Server setup - check for SSL and build redirect from non-SSL if SSL
* has been set up
*/
var server;
var sslOpts = CONFIG.SERVER("SSL_CERTS");
if (sslOpts) {
// Create an HTTP redirect
var redirecter = http.createServer(function(req, res) {
res.writeHead(200);
var server = "https://" + req.headers.host.replace(/:\d*/, "")
, port = CONFIG.SERVER('SSL_SILENT_REDIRECT') ? "" : ":" + app.get('port')
, url = req.url
, newPath = server + port + url;
log('SSL Silent', CONFIG.SERVER('SSL_SILENT_REDIRECT'));
res.writeHead(302, {
'Location': newPath
});
res.end("Redirecting to secure server at " + newPath);
});
redirecter.listen(app.get('www_port'));
// Use SSL for our real server
app.set('port', app.get('ssl_port'));
https = require("https");
server = https.createServer(sslOpts, app);
}
else {
// No SSL Configuration found
app.set('port', app.get('www_port'));
server = http.createServer(app);
}
server.listen(app.get('port'), function(){
// Change effective user / group to whoever owns the file
fs.stat(__filename, function(err, stats) {
// Avoid exceptions
try {
process.setgid(stats.gid);
process.setuid(stats.uid);
}
catch (err) { }
log("Effective uid", process.getuid());
});
log("Express server listening on port", app.get('port'));
});
/**
* Now & Socket IO
*/
// Port is a necessity due to a very odd bug in nowjs where it assumes port 80 even if using the
// https protocol
var everyone = nowjs.initialize(server, {port: app.get('port')});
// Function fired on connect using websockets
nowjs.on('connect', function(){
log("Client Initializing Connection", this.user.clientId);
// Alias this for use in sessionStore callback scope
var that = this;
// Get the user's session so we can (fairly securely) set up user name, type, and room
sessionAPI.getSession(that, sessionStore, function(err, session) {
// If we had any errors, exit here
if (err) {
logger.error("Error retrieving session for nowjs connection [connect]: " + err.message);
return;
}
// All users should have a type set on session
that.now.type = session.type;
// Only authenticated users currently have name stored in session
if (session.user) {
that.now.name = session.user.name;
}
that.now.room = session.room;
// Join the room. We don't attempt any error handling on this, as the session shouldn't
// be set without already having checked the validity of the room
var id = that.user.clientId;
var group = nowjs.getGroup(session.room);
group.addUser(id);
// If this is the presenter, set presenter on the group object
if (session.type == "presenter") {
group.presenter = id;
}
logger.debugPair(id + " entered", that.now.room);
logger.debugPair("[nowjs.on] connect now object", util.inspect(that.now));
// Connect Initialization Function to run.
nowjs.getClient(that.user.clientId, function(){
if(that.now.hasOwnProperty('initialize') ) that.now.initialize();
})
});
});
// Function fired on close of websockets
nowjs.on('disconnect', function(){
logger.debugPair("Client disconnected " + this.user.clientId, this.now.name);
});
everyone.now.setName = function(newName) {
var that = this;
sessionAPI.getSession(that, sessionStore, function(err, session) {
// Report session errors and exit
if (err) {
logger.error("Error retrieving session for nowjs connection [setName]: " + err.message);
return;
}
// Make sure we have session.user object
if (!session.user) {
session.user = {};
}
// Avoid changing a name that's already been set
if (session.user.name && session.user.name !== "" && session.user.name !== newName) {
logger.warn("Skipping name change for user: " + session.user.name +
" (trying to change to " + newName + ")");
return;
}
// Set session name and nowjs name
logger.debug("Setting name for client " + that.user.clientId + " to " + newName);
session.user.name = newName;
that.now.name = newName;
});
};
/**
* Clear Client's UI & Now question variables
*/
everyone.now.distributeClear = function(){
var that = this;
sessionAPI.getSession(that, sessionStore, function(err, session) {
// If we had any errors, exit here
if (err) {
logger.error("Error retrieving session for nowjs connection [distributeClear]: " + err.message);
return;
}
if (session.type !== "presenter") {
logger.error("Non-presenter trying to call distributeClear()!");
return;
}
logger.debugPair('Question Clear Sent', that.user.clientId);
logger.debugPair('distributeClear Now Object', util.inspect(that.now) );
var group = nowjs.getGroup(session.room);
group.now.clientClearQuestion();
delete group.question;
delete group.now.question;
delete group.receiveAnswer;
});
}
/**
* Stop Session sent to client
*/
everyone.now.distributeStopSession = function(){
var that = this;
sessionAPI.getSession(that, sessionStore, function(err, session) {
// If we had any errors, exit here
if (err) {
logger.error("Error retrieving session for nowjs connection [distributeClear]: " + err.message);
return;
}
if (session.type !== "presenter") {
logger.error("Non-presenter trying to call distributeClear()!");
return;
}
logger.debug('Presenter Closed Session');
var group = nowjs.getGroup(session.room);
group.now.clientStopSession();
delete group.question;
delete group.now.question;
delete group.receiveAnswer;
});
}
/**
* Send out a message
* @param {String} message The text to be sent to entire virtual room
*/
everyone.now.distributeMessage = function(message){
var that = this;
sessionAPI.getSession(that, sessionStore, function(err, session) {
if (err) {
logger.error("Error retrieving session for nowjs connection [distributeMessage]: " + err.message);
return;
}
// Send message
nowjs.getGroup(session.room).now.receiveMessage(session.user.name, message);
});
};
/**
* Send out a question to virtual room
* @param {Object} question Question object with all client needed variables to display question to client.
*/
everyone.now.distributeQuestion = function(question){
var that = this;
sessionAPI.getSession(that, sessionStore, function(err, session) {
// If we had any errors, exit here
if (err) {
logger.error("Error retrieving session for nowjs connection [distributeQuestion]: " + err.message);
return;
}
if (session.type !== "presenter") {
logger.error("Non-presenter trying to call distributeQuestion()!");
return;
}
// Set current room
var currentRoom = session.room;
// Determine Group in virtual room
var group = nowjs.getGroup(currentRoom);
var recQFn = group.now.hasOwnProperty('receiveQuestion') || '' ;
logger.debugPair("[distributeQuestion] Group", util.inspect(group) );
// Allow reception of answers
group.receiveAnswer = true;
// Set Question Session ID
question.qSessionID = session.rippleSession.id;
logger.debugPair("[distributeQuestion] Room", currentRoom);
logger.debugPair("[distributeQuestion] now object", util.inspect(that.now) );
// Save Question to db
logger.debugPair('Question Sent to db', new Date());
SSM.questionSent(question, function(qID, sessionID){
question.qID = String(qID);
question.qSessionID = String(sessionID);
// Set Expiration Time
var expireProp = 'expireTime'
, rippleSession = session.rippleSession
, hasExpire = rippleSession.hasOwnProperty(expireProp) && rippleSession[expireProp] != ""
, isSetExpire = group.now.hasOwnProperty(expireProp)
logger.debugPair("distributeQuestion hasExpire", hasExpire);
logger.debugPair("distributeQuestion isSetExpire", isSetExpire);
if( hasExpire && !isSetExpire ) {
question[expireProp] = rippleSession[expireProp];
}
// Server Side Question Object held private from client
group.question = question;
// Send Question to Client
group.now.question = {
type: question.type,
qTxt: question.qTxt,
authorID: question.authorID,
qOptions: question.qOptions,
qID: question.qID,
}
group.now.receiveQuestion();
logger.debugPair('Question Sent to client', util.inspect(group.now.question) );
if(recQFn) recQFn(
session.user.name
);
logger.debugPair("");
log('Question Sent Complete', new Date() );
logger.debugPair("");
});
/**
* Hook fired when a question is distributed.
*
* @event distribute
* @for plugin-server.question
* @param {String} room The room that the question was distributed to
* @param {Object} question A object that contains all the question information
*/
plugin.invokeAll("question.distribute", currentRoom, question);
});
};
/**
* Client sent in answer
* @param {Object} data Object that contains answer information
*/
everyone.now.distributeAnswer = function(data){
var that = this;
// Ensure we have data before trying to use it
if (!data.qID) {
logger.warn("Invalid answer submission - missing Question ID");
return;
}
if (!data.hasOwnProperty('answer')) {
logger.warn("Invalid answer submission - missing answer");
return;
}
sessionAPI.getSession(that, sessionStore, function(err, session) {
var room = session.room
, nowName = nowjs.users[that.user.clientId].now.name
, hasSessionName = session.hasOwnProperty('user') && session.user.hasOwnProperty('name')
, name = hasSessionName ? session.user.name : nowName
, group = nowjs.getGroup(room)
, answer = data.answer
, qID = data.qID;
log('Answer Sent ' + name + '\n [' + that.user.clientId + ']', util.inspect(data));
// If we had any errors, exit here
if (err) {
logger.error("Error retrieving session for nowjs connection [distributeAnswer]: " + err.message);
return;
}
// Ensure that answer are allowed to be received
if (!group.hasOwnProperty('receiveAnswer') || !group.receiveAnswer ){
logger.warn("Invalid answer submission - currently not taking answers");
return;
}
// Clean data
answer = cleanAnswer(answer, group.question);
// Verify qID
if (group.question.qID != qID) {
logger.info("Answer submitted for wrong qID");
return;
}
// Send Answer to Admin
var presenter = nowjs.users[group.presenter];
presenter.now.receiveAnswer(that.user.clientId, name, answer);
// Save Answer to DB
SSM.saveAnswer(answer, group.question, name)
// Send Answer to plugins
/**
* Hook fired when a answer is received.
*
* @event distribute
* @for plugin-server.answer
* @param {String} room The room that the question was distributed to
* @param {String} clientID The client's id who submitted the answer
* @param {String} name The client's name who submitted the answer
* @param {Object} question An object with all the question information
* @param {String} answer The answer submitted by the client
*/
plugin.invokeAll("answer.distribute", room, that.user.clientId, name, group.question, answer);
});
};
/**
* Santize Answers
* @param {Object} answer Answer from client
* @param {Object} question Question responded to
* @return {Object} cleanAnswer Santized answer
*/
var cleanAnswer = function(answer, question){
var translation = 'string';
// Remove XSS vulnerbilities
answer = sanitize(answer).xss();
// Truncate long answers first to avoid DB storage issues, logging giant strings, and sending
// huge blobs of data to presenter
if (answer.length > 500) {
answer = answer.substring(0, 500);
}
// For security reason, answer is stongly typed
switch(question.type){
case 'slider':
translation = 'numeric';
break;
case 'numeric':
translation = 'numeric';
break;
}
switch(translation){
case 'string':
// Make sure it is a string
answer = String( answer ).replace(/<\/?[^>]+(>|$)/g, "")
//answer = sanitize( answer ).escape();
break;
case 'numeric':
answer = sanitize( answer ).toFloat();
}
logger.debugPair("Answer typed to", translation);
return answer;
}
/**
* Set whether answers can or can not be received
* @param {String} status The current status of polling
*/
everyone.now.distributePolling = function(status){
var that = this;
sessionAPI.getSession(that, sessionStore, function(err, session) {
// If we had any errors, exit here
if (err) {
logger.error("Error retrieving session for nowjs connection [distributePolling]: " + err.message);
return;
}
if (session.type !== "presenter") {
logger.error("Non-presenter trying to call distributePolling()!");
return;
}
var group = nowjs.getGroup(session.room)
group.now.clientSetPolling(status);
group.now.question.polling = status;
// Determine reception of answers status
if( status === 'on' ) group.receiveAnswer = true;
else group.receiveAnswer = false;
});
}
console.log("==============================================");
console.log("Ripple app.js executed");
console.log("==============================================");
// Graceful Shutdown
process.once('SIGINT', function () {
console.log("\nGracefully shutting down from SIGINT (Ctrl+C)");
// Remove pid file
fs.unlinkSync('custom/pid.txt');
process.exit();
});
} // end "run" function