diff --git a/app.js b/app.js
index 05ba01c22f..4324d6f2e4 100644
--- a/app.js
+++ b/app.js
@@ -55,7 +55,7 @@
winston.info('');
- if (fs.existsSync(__dirname + '/config.json') && (!nconf.get('setup') && !nconf.get('upgrade'))) {
+ if (!nconf.get('help') && !nconf.get('setup') && !nconf.get('upgrade') && fs.existsSync(__dirname + '/config.json')) {
// Load server-side configs
nconf.file({
file: __dirname + '/config.json'
@@ -66,7 +66,7 @@
nconf.set('upload_url', nconf.get('url') + 'uploads/');
winston.info('Initializing NodeBB v' + pkg.version + ', on port ' + nconf.get('port') + ', using Redis store at ' + nconf.get('redis:host') + ':' + nconf.get('redis:port') + '.');
- winston.info('NodeBB instance bound to: ' + (nconf.get('bind_address') || 'Any address'));
+ winston.info('NodeBB instance bound to: ' + ((nconf.get('bind_address') === "0.0.0.0" || !nconf.get('bind_address')) ? 'Any address (0.0.0.0)' : nconf.get('bind_address')));
if (process.env.NODE_ENV === 'development') {
winston.info('Base Configuration OK.');
@@ -87,7 +87,8 @@
SocketIO = require('socket.io').listen(global.server, { log: false, transports: ['websocket', 'xhr-polling', 'jsonp-polling', 'flashsocket']}),
websockets = require('./src/websockets.js'),
posts = require('./src/posts.js'),
- plugins = require('./src/plugins'); // Don't remove this - plugins initializes itself
+ plugins = require('./src/plugins'), // Don't remove this - plugins initializes itself
+ Notifications = require('./src/notifications');
websockets.init(SocketIO);
@@ -106,18 +107,10 @@
]);
templates.ready(webserver.init);
- });
-
- } else if (nconf.get('upgrade')) {
- nconf.file({
- file: __dirname + '/config.json'
- });
- meta = require('./src/meta.js');
- meta.configs.init(function () {
- require('./src/upgrade').upgrade();
+ Notifications.init();
});
- } else {
+ } else if (nconf.get('setup') || !fs.existsSync(__dirname + '/config.json')) {
// New install, ask setup questions
if (nconf.get('setup')) {
winston.info('NodeBB Setup Triggered via Command Line');
@@ -135,10 +128,29 @@
if (err) {
winston.error('There was a problem completing NodeBB setup: ', err.message);
} else {
- winston.info('NodeBB Setup Completed.');
+ winston.info('NodeBB Setup Completed. Run \'node app\' to manually start your NodeBB server.');
}
process.exit();
});
- }
-}());
\ No newline at end of file
+
+ } else if (nconf.get('upgrade')) {
+ nconf.file({
+ file: __dirname + '/config.json'
+ });
+ meta = require('./src/meta.js');
+
+ meta.configs.init(function () {
+ require('./src/upgrade').upgrade();
+ });
+ } else/* if (nconf.get('help') */{
+ winston.info('Usage: node app [options] [arguments]');
+ winston.info(' [NODE_ENV=development | NODE_ENV=production] node app [--start] [arguments]');
+ winston.info('');
+ winston.info('Options:');
+ winston.info(' --help displays this usage information');
+ winston.info(' --setup configure your environment and setup NodeBB');
+ winston.info(' --upgrade upgrade NodeBB, first read: github.com/designcreateplay/NodeBB/wiki/Upgrading-NodeBB');
+ winston.info(' --start manually start NodeBB (default when no options are given)');
+ };
+}());
diff --git a/package.json b/package.json
index 89d3e9b69c..4890e1acd5 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,8 @@
"nodebb-plugin-mentions": "~0.1.13",
"nodebb-plugin-markdown": "~0.1.7",
"nodebb-theme-vanilla": "designcreateplay/nodebb-theme-vanilla",
- "nodebb-theme-cerulean": "0.0.3"
+ "nodebb-theme-cerulean": "0.0.3",
+ "cron": "~1.0.1"
},
"optionalDependencies": {
"hiredis": "~0.1.15"
diff --git a/public/src/forum/footer.js b/public/src/forum/footer.js
index 45cc71312c..fa2240a93c 100644
--- a/public/src/forum/footer.js
+++ b/public/src/forum/footer.js
@@ -101,6 +101,7 @@
numUnread = data.unread.length,
x;
notifList.innerHTML = '';
+ console.log(data);
if ((data.read.length + data.unread.length) > 0) {
for (x = 0; x < numUnread; x++) {
notifEl.setAttribute('data-nid', data.unread[x].nid);
@@ -115,15 +116,16 @@
notifFrag.appendChild(notifEl.cloneNode(true));
}
} else {
+ notifEl.className = 'no-notifs';
notifEl.innerHTML = 'You have no notifications';
- notifFrag.appendChild(notifEl);
+ notifFrag.appendChild(notifEl.cloneNode(true));
}
// Add dedicated link to /notifications
notifEl.removeAttribute('data-nid');
notifEl.className = 'pagelink';
notifEl.innerHTML = 'See all Notifications';
- notifFrag.appendChild(notifEl);
+ notifFrag.appendChild(notifEl.cloneNode(true));
notifList.appendChild(notifFrag);
diff --git a/public/src/utils.js b/public/src/utils.js
index 9f6a9f3c12..71d695fffa 100644
--- a/public/src/utils.js
+++ b/public/src/utils.js
@@ -11,7 +11,7 @@
generateUUID: function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0,
- v = c == 'x' ? r : (r & 0x3 | 0x8);
+ v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
},
@@ -225,4 +225,4 @@
module: {
exports: {}
}
-} : module)
\ No newline at end of file
+} : module);
\ No newline at end of file
diff --git a/public/templates/header.tpl b/public/templates/header.tpl
index 73784aae2c..08619acdbd 100644
--- a/public/templates/header.tpl
+++ b/public/templates/header.tpl
@@ -24,9 +24,6 @@
"forum": '../forum'
}
});
- requirejs.onError = function(err) {
- console.log(err);
- }
diff --git a/src/notifications.js b/src/notifications.js
index d6b2791028..f6d4f03e06 100644
--- a/src/notifications.js
+++ b/src/notifications.js
@@ -1,16 +1,30 @@
var RDB = require('./redis.js'),
async = require('async'),
utils = require('../public/src/utils.js'),
+ winston = require('winston'),
+ cron = require('cron').CronJob,
notifications = {
+ init: function() {
+ if (process.env.NODE_ENV === 'development') {
+ winston.info('[notifications.init] Registering jobs.');
+ }
+
+ new cron('0 0 * * *', notifications.prune, null, true);
+ },
get: function(nid, uid, callback) {
RDB.multi()
.hmget('notifications:' + nid, 'text', 'score', 'path', 'datetime', 'uniqueId')
.zrank('uid:' + uid + ':notifications:read', nid)
+ .exists('notifications:' + nid)
.exec(function(err, results) {
- var notification = results[0]
+ var notification = results[0],
readIdx = results[1];
+ if (!results[2]) {
+ return callback(null);
+ }
+
callback({
nid: nid,
text: notification[0],
@@ -30,16 +44,32 @@ var RDB = require('./redis.js'),
* the new one put in its place.
*/
RDB.incr('notifications:next_nid', function(err, nid) {
+ RDB.sadd('notifications', nid);
RDB.hmset('notifications:' + nid, {
text: text || '',
path: path || null,
datetime: Date.now(),
uniqueId: uniqueId || utils.generateUUID()
}, function(err, status) {
- if (!err) callback(nid);
+ if (!err) {
+ callback(nid);
+ }
});
});
},
+ destroy: function(nid) {
+ var multi = RDB.multi();
+
+ multi.del('notifications:' + nid);
+ multi.srem('notifications', nid);
+
+ multi.exec(function(err) {
+ if (err) {
+ winston.error('Problem deleting expired notifications. Stack follows.');
+ winston.error(err.stack);
+ }
+ });
+ },
push: function(nid, uids, callback) {
if (!Array.isArray(uids)) uids = [uids];
@@ -48,12 +78,14 @@ var RDB = require('./redis.js'),
notifications.get(nid, null, function(notif_data) {
for (x = 0; x < numUids; x++) {
- if (parseInt(uids[x]) > 0) {
+ if (parseInt(uids[x], 10) > 0) {
(function(uid) {
notifications.remove_by_uniqueId(notif_data.uniqueId, uid, function() {
RDB.zadd('uid:' + uid + ':notifications:unread', notif_data.datetime, nid);
global.io.sockets.in('uid_' + uid).emit('event:new_notification');
- if (callback) callback(true);
+ if (callback) {
+ callback(true);
+ }
});
})(uids[x]);
}
@@ -67,13 +99,18 @@ var RDB = require('./redis.js'),
if (nids && nids.length > 0) {
async.each(nids, function(nid, next) {
notifications.get(nid, uid, function(nid_info) {
- if (nid_info.uniqueId === uniqueId) RDB.zrem('uid:' + uid + ':notifications:unread', nid);
+ if (nid_info.uniqueId === uniqueId) {
+ RDB.zrem('uid:' + uid + ':notifications:unread', nid);
+ }
+
next();
});
}, function(err) {
next();
});
- } else next();
+ } else {
+ next();
+ }
});
},
function(next) {
@@ -81,17 +118,24 @@ var RDB = require('./redis.js'),
if (nids && nids.length > 0) {
async.each(nids, function(nid, next) {
notifications.get(nid, uid, function(nid_info) {
- if (nid_info.uniqueId === uniqueId) RDB.zrem('uid:' + uid + ':notifications:read', nid);
+ if (nid_info.uniqueId === uniqueId) {
+ RDB.zrem('uid:' + uid + ':notifications:read', nid);
+ }
+
next();
});
}, function(err) {
next();
});
- } else next();
+ } else {
+ next();
+ }
});
}
], function(err) {
- if (!err) callback(true);
+ if (!err) {
+ callback(true);
+ }
});
},
mark_read: function(nid, uid, callback) {
@@ -99,38 +143,126 @@ var RDB = require('./redis.js'),
notifications.get(nid, uid, function(notif_data) {
RDB.zrem('uid:' + uid + ':notifications:unread', nid);
RDB.zadd('uid:' + uid + ':notifications:read', notif_data.datetime, nid);
- if (callback) callback();
+ if (callback) {
+ callback();
+ }
});
}
},
mark_read_multiple: function(nids, uid, callback) {
- if (!Array.isArray(nids) && parseInt(nids, 10) > 0) nids = [nids];
+ if (!Array.isArray(nids) && parseInt(nids, 10) > 0) {
+ nids = [nids];
+ }
async.each(nids, function(nid, next) {
notifications.mark_read(nid, uid, function(err) {
- if (!err) next(null);
+ if (!err) {
+ next(null);
+ }
});
}, function(err) {
- if (callback) callback(err);
+ if (callback) {
+ callback(err);
+ }
});
},
mark_all_read: function(uid, callback) {
RDB.zrange('uid:' + uid + ':notifications:unread', 0, 10, function(err, nids) {
- if (err) return callback(err);
+ if (err) {
+ return callback(err);
+ }
if (nids.length > 0) {
notifications.mark_read_multiple(nids, uid, function(err) {
callback(err);
});
- } else callback();
+ } else {
+ callback();
+ }
+ });
+ },
+ prune: function(cutoff) {
+ if (process.env.NODE_ENV === 'development') {
+ winston.info('[notifications.prune] Removing expired notifications from the database.');
+ }
+
+ var today = new Date(),
+ numPruned = 0;
+
+ if (!cutoff) {
+ cutoff = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7);
+ }
+
+ var cutoffTime = cutoff.getTime();
+
+ async.parallel({
+ "inboxes": function(next) {
+ RDB.keys('uid:*:notifications:unread', next);
+ },
+ "nids": function(next) {
+ RDB.smembers('notifications', function(err, nids) {
+ async.filter(nids, function(nid, next) {
+ RDB.hget('notifications:' + nid, 'datetime', function(err, datetime) {
+ if (parseInt(datetime, 10) < cutoffTime) {
+ next(true);
+ } else {
+ next(false);
+ }
+ });
+ }, function(expiredNids) {
+ next(null, expiredNids);
+ });
+ });
+ }
+ }, function(err, results) {
+ if (!err) {
+ var numInboxes = results.inboxes.length,
+ x;
+
+ async.eachSeries(results.nids, function(nid, next) {
+ var multi = RDB.multi();
+
+ for(x=0;x 0) {
async.eachSeries(nids, function(nid, next) {
notifications.get(nid, uid, function(notif_data) {
- unread.push(notif_data);
+ // If the notification could not be found, silently drop it
+ if (notif_data) {
+ unread.push(notif_data);
+ } else {
+ RDB.zrem('uid:' + uid + ':notifications:unread', nid);
+ }
+
next();
});
}, function(err) {
@@ -925,7 +931,13 @@ var utils = require('./../public/src/utils.js'),
if (nids && nids.length > 0) {
async.eachSeries(nids, function(nid, next) {
notifications.get(nid, uid, function(notif_data) {
- read.push(notif_data);
+ // If the notification could not be found, silently drop it
+ if (notif_data) {
+ read.push(notif_data);
+ } else {
+ RDB.zrem('uid:' + uid + ':notifications:read', nid);
+ }
+
next();
});
}, function(err) {
diff --git a/src/webserver.js b/src/webserver.js
index 94e662fd4e..c4502787f0 100644
--- a/src/webserver.js
+++ b/src/webserver.js
@@ -136,6 +136,10 @@ var express = require('express'),
app.use(function (req, res, next) {
nconf.set('https', req.secure);
res.locals.csrf_token = req.session._csrf;
+
+ // Disable framing
+ res.setHeader("X-Frame-Options", "DENY");
+
next();
});
@@ -667,6 +671,10 @@ var express = require('express'),
});
});
+ // Debug routes
+ if (process.env.NODE_ENV === 'development') {
+ require('./routes/debug')(app);
+ }
var custom_routes = {
'routes': [],
diff --git a/tests/.jshintrc b/tests/.jshintrc
new file mode 100644
index 0000000000..3bc176b7f6
--- /dev/null
+++ b/tests/.jshintrc
@@ -0,0 +1,9 @@
+{
+ "strict" : false, // true: Requires all functions run in ES5 Strict Mode
+
+ // Custom Globals
+ "globals" : {
+ "it": false,
+ "describe": false
+ }
+}
\ No newline at end of file
diff --git a/tests/utils.js b/tests/utils.js
index 70643802f8..ec88aa24c1 100644
--- a/tests/utils.js
+++ b/tests/utils.js
@@ -8,5 +8,28 @@ describe("Utility Methods", function(){
var username = "John\"'-. Doeäâèéë1234";
assert(utils.isUserNameValid(username), 'invalid username');
});
+ it("rejects empty string", function(){
+ var username = "";
+ assert.ifError(utils.isUserNameValid(username), 'accepted as valid username');
+ });
+ });
+
+ describe("email validation", function(){
+ it("accepts sample address", function(){
+ var email = 'sample@example.com';
+ assert(utils.isEmailValid(email), 'invalid email');
+ });
+ it("rejects empty address", function(){
+ var email = '';
+ assert.ifError(utils.isEmailValid(email), 'accepted as valid email');
+ });
+ });
+
+ describe("UUID generation", function(){
+ it("return unique random value every time", function(){
+ var uuid1 = utils.generateUUID(),
+ uuid2 = utils.generateUUID();
+ assert.notEqual(uuid1, uuid2, "matches");
+ });
});
});