diff --git a/src/controllers/categories.js b/src/controllers/categories.js index 8112dead67..d5cd04b504 100644 --- a/src/controllers/categories.js +++ b/src/controllers/categories.js @@ -2,7 +2,6 @@ var async = require('async'); var nconf = require('nconf'); -var validator = require('validator'); var categories = require('../categories'); var meta = require('../meta'); @@ -13,10 +12,10 @@ var categoriesController = {}; categoriesController.list = function (req, res, next) { res.locals.metaTags = [{ name: 'title', - content: validator.escape(String(meta.config.title || 'NodeBB')), + content: String(meta.config.title || 'NodeBB'), }, { name: 'description', - content: validator.escape(String(meta.config.description || '')), + content: String(meta.config.description || ''), }, { property: 'og:title', content: '[[pages:categories]]', diff --git a/src/controllers/index.js b/src/controllers/index.js index 1ec1696e21..59f5527ef4 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -201,37 +201,35 @@ Controllers.registerInterstitial = function (req, res, next) { return res.redirect(nconf.get('relative_path') + '/register'); } - plugins.fireHook('filter:register.interstitial', { - userData: req.session.registration, - interstitials: [], - }, function (err, data) { - if (err) { - return next(err); - } - - if (!data.interstitials.length) { - // No interstitials, redirect to home - delete req.session.registration; - return res.redirect('/'); - } - - var renders = data.interstitials.map(function (interstitial) { - return async.apply(req.app.render.bind(req.app), interstitial.template, interstitial.data || {}); - }); - var errors = req.flash('error'); - - async.parallel(renders, function (err, sections) { - if (err) { - return next(err); + async.waterfall([ + function (next) { + plugins.fireHook('filter:register.interstitial', { + userData: req.session.registration, + interstitials: [], + }, next); + }, + function (data, next) { + if (!data.interstitials.length) { + // No interstitials, redirect to home + delete req.session.registration; + return res.redirect('/'); } + var renders = data.interstitials.map(function (interstitial) { + return async.apply(req.app.render.bind(req.app), interstitial.template, interstitial.data || {}); + }); + + async.parallel(renders, next); + }, + function (sections) { + var errors = req.flash('error'); res.render('registerComplete', { title: '[[pages:registration-complete]]', errors: errors, sections: sections, }); - }); - }); + }, + ], next); }; Controllers.compose = function (req, res, next) { diff --git a/src/meta/errors.js b/src/meta/errors.js index 085cb84fea..fb169764ba 100644 --- a/src/meta/errors.js +++ b/src/meta/errors.js @@ -1,5 +1,6 @@ 'use strict'; +var async = require('async'); var validator = require('validator'); var db = require('../database'); @@ -16,18 +17,19 @@ module.exports = function (Meta) { }; Meta.errors.get = function (escape, callback) { - db.getSortedSetRevRangeWithScores('errors:404', 0, -1, function (err, data) { - if (err) { - return callback(err); - } - - data = data.map(function (nfObject) { - nfObject.value = escape ? validator.escape(String(nfObject.value || '')) : nfObject.value; - return nfObject; - }); - - callback(null, data); - }); + async.waterfall([ + function (next) { + db.getSortedSetRevRangeWithScores('errors:404', 0, -1, next); + }, + function (data, next) { + data = data.map(function (nfObject) { + nfObject.value = escape ? validator.escape(String(nfObject.value || '')) : nfObject.value; + return nfObject; + }); + + next(null, data); + }, + ], callback); }; Meta.errors.clear = function (callback) { diff --git a/src/plugins/hooks.js b/src/plugins/hooks.js index f8f5f6bc44..e72b233940 100644 --- a/src/plugins/hooks.js +++ b/src/plugins/hooks.js @@ -78,6 +78,13 @@ module.exports = function (Plugins) { } }; + Plugins.unregisterHook = function (id, hook, method) { + var hooks = Plugins.loadedHooks[hook] || []; + Plugins.loadedHooks[hook] = hooks.filter(function (hookData) { + return hookData && hookData.id !== id && hookData.method !== method; + }); + }; + Plugins.fireHook = function (hook, params, callback) { callback = typeof callback === 'function' ? callback : function () {}; diff --git a/src/posts.js b/src/posts.js index 2f9132a25b..83b8e39a1f 100644 --- a/src/posts.js +++ b/src/posts.js @@ -248,6 +248,9 @@ var plugins = require('./plugins'); }, ], next); }, + function (next) { + db.sortedSetAdd('posts:votes', postData.votes, postData.pid, next); + }, function (next) { Posts.setPostFields(postData.pid, { upvotes: postData.upvotes, downvotes: postData.downvotes }, next); }, diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index 44a44f28e7..1b4d9ebada 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -93,17 +93,18 @@ SocketAdmin.themes.set = function (socket, data, callback) { return callback(new Error('[[error:invalid-data]]')); } - var wrappedCallback = function (err) { - if (err) { - return callback(err); - } - meta.themes.set(data, callback); - }; - if (data.type === 'bootswatch') { - wrappedCallback(); - } else { - widgets.reset(wrappedCallback); - } + async.waterfall([ + function (next) { + if (data.type === 'bootswatch') { + setImmediate(next); + } else { + widgets.reset(next); + } + }, + function (next) { + meta.themes.set(data, next); + }, + ], callback); }; SocketAdmin.plugins.toggleActive = function (socket, plugin_id, callback) { @@ -125,7 +126,7 @@ SocketAdmin.plugins.orderActivePlugins = function (socket, data, callback) { if (plugin && plugin.name) { db.sortedSetAdd('plugins:active', plugin.order || 0, plugin.name, next); } else { - next(); + setImmediate(next); } }, callback); }; @@ -148,7 +149,7 @@ SocketAdmin.config.set = function (socket, data, callback) { } var _data = {}; _data[data.key] = data.value; - SocketAdmin.config.setMultiple(socket, data, callback); + SocketAdmin.config.setMultiple(socket, _data, callback); }; SocketAdmin.config.setMultiple = function (socket, data, callback) { @@ -205,6 +206,10 @@ SocketAdmin.email.test = function (socket, data, callback) { }; SocketAdmin.analytics.get = function (socket, data, callback) { + if (!data || !data.graph || !data.units) { + return callback(new Error('[[error:invalid-data]]')); + } + // Default returns views from past 24 hours, by hour if (data.units === 'days') { data.amount = 30; @@ -212,34 +217,30 @@ SocketAdmin.analytics.get = function (socket, data, callback) { data.amount = 24; } - if (data && data.graph && data.units && data.amount) { - if (data.graph === 'traffic') { - async.parallel({ - uniqueVisitors: function (next) { - if (data.units === 'days') { - analytics.getDailyStatsForSet('analytics:uniquevisitors', data.until || Date.now(), data.amount, next); - } else { - analytics.getHourlyStatsForSet('analytics:uniquevisitors', data.until || Date.now(), data.amount, next); - } - }, - pageviews: function (next) { - if (data.units === 'days') { - analytics.getDailyStatsForSet('analytics:pageviews', data.until || Date.now(), data.amount, next); - } else { - analytics.getHourlyStatsForSet('analytics:pageviews', data.until || Date.now(), data.amount, next); - } - }, - monthlyPageViews: function (next) { - analytics.getMonthlyPageViews(next); - }, - }, function (err, data) { - data.pastDay = data.pageviews.reduce(function (a, b) { return parseInt(a, 10) + parseInt(b, 10); }); - data.pageviews[data.pageviews.length - 1] = parseInt(data.pageviews[data.pageviews.length - 1], 10) + analytics.getUnwrittenPageviews(); - callback(err, data); - }); - } - } else { - callback(new Error('Invalid analytics call')); + if (data.graph === 'traffic') { + async.parallel({ + uniqueVisitors: function (next) { + if (data.units === 'days') { + analytics.getDailyStatsForSet('analytics:uniquevisitors', data.until || Date.now(), data.amount, next); + } else { + analytics.getHourlyStatsForSet('analytics:uniquevisitors', data.until || Date.now(), data.amount, next); + } + }, + pageviews: function (next) { + if (data.units === 'days') { + analytics.getDailyStatsForSet('analytics:pageviews', data.until || Date.now(), data.amount, next); + } else { + analytics.getHourlyStatsForSet('analytics:pageviews', data.until || Date.now(), data.amount, next); + } + }, + monthlyPageViews: function (next) { + analytics.getMonthlyPageViews(next); + }, + }, function (err, data) { + data.pastDay = data.pageviews.reduce(function (a, b) { return parseInt(a, 10) + parseInt(b, 10); }); + data.pageviews[data.pageviews.length - 1] = parseInt(data.pageviews[data.pageviews.length - 1], 10) + analytics.getUnwrittenPageviews(); + callback(err, data); + }); } }; @@ -260,13 +261,15 @@ SocketAdmin.deleteAllEvents = function (socket, data, callback) { }; SocketAdmin.getSearchDict = function (socket, data, callback) { - user.getSettings(socket.uid, function (err, settings) { - if (err) { - return callback(err); - } - var lang = settings.userLang || meta.config.defaultLang || 'en-GB'; - getAdminSearchDict(lang, callback); - }); + async.waterfall([ + function (next) { + user.getSettings(socket.uid, next); + }, + function (settings, next) { + var lang = settings.userLang || meta.config.defaultLang || 'en-GB'; + getAdminSearchDict(lang, next); + }, + ], callback); }; SocketAdmin.deleteAllSessions = function (socket, data, callback) { diff --git a/src/upgrade.js b/src/upgrade.js index 847197357d..80aa38206f 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -46,7 +46,7 @@ var Upgrade = { }, { version: 'master', // rename this to whenever the next NodeBB version is (non-breaking) - upgrades: ['sound_settings'], + upgrades: ['sound_settings', 'post_votes_zset'], }, { version: 'develop', // rename this to whatever the next NodeBB version is (breaking) diff --git a/src/upgrades/post_votes_zset.js b/src/upgrades/post_votes_zset.js new file mode 100644 index 0000000000..c1eefb6688 --- /dev/null +++ b/src/upgrades/post_votes_zset.js @@ -0,0 +1,26 @@ +/* jslint node: true */ + +'use strict'; + +var db = require('../database'); + +var async = require('async'); + +module.exports = { + name: 'New sorted set posts:votes', + timestamp: Date.UTC(2017, 1, 27), + method: function (callback) { + require('../batch').processSortedSet('posts:pid', function (pids, next) { + async.each(pids, function (pid, next) { + db.getObjectFields('post:' + pid, ['upvotes', 'downvotes'], function (err, postData) { + if (err || !postData) { + return next(err); + } + + var votes = parseInt(postData.upvotes || 0, 10) - parseInt(postData.downvotes || 0, 10); + db.sortedSetAdd('posts:votes', votes, pid, next); + }); + }, next); + }, {}, callback); + }, +}; diff --git a/test/controllers.js b/test/controllers.js index c456d3fd69..d79235d16f 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -136,6 +136,59 @@ describe('Controllers', function () { }); }); + it('should load /register/complete', function (done) { + var plugins = require('../src/plugins'); + function hookMethod(data, next) { + data.interstitials.push({ template: 'topic.tpl', data: {} }); + next(null, data); + } + + plugins.registerHook('myTestPlugin', { + hook: 'filter:register.interstitial', + method: hookMethod, + }); + + var data = { + username: 'interstitial', + password: '123456', + email: 'test@me.com', + }; + + var jar = request.jar(); + request({ + url: nconf.get('url') + '/api/config', + json: true, + jar: jar, + }, function (err, response, body) { + assert.ifError(err); + + request.post(nconf.get('url') + '/register', { + form: data, + json: true, + jar: jar, + headers: { + 'x-csrf-token': body.csrf_token, + }, + }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert.equal(body.referrer, nconf.get('relative_path') + '/register/complete'); + request(nconf.get('url') + '/api/register/complete', { + jar: jar, + json: true, + }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body.sections); + assert(body.errors); + assert(body.title); + plugins.unregisterHook('myTestPlugin', 'filter:register.interstitial', hookMethod); + done(); + }); + }); + }); + }); + it('should load /robots.txt', function (done) { request(nconf.get('url') + '/robots.txt', function (err, res, body) { assert.ifError(err); @@ -471,7 +524,7 @@ describe('Controllers', function () { hidden: 1, }, function (err) { assert.ifError(err); - request(nconf.get('url') + '/groups/hidden-group/members', function (err, res, body) { + request(nconf.get('url') + '/groups/hidden-group/members', function (err, res) { assert.ifError(err); assert.equal(res.statusCode, 404); done(); @@ -531,7 +584,7 @@ describe('Controllers', function () { headers: { 'x-csrf-token': csrf_token, }, - }, function (err, res, body) { + }, function (err, res) { assert.ifError(err); assert.equal(res.statusCode, 404); done(); @@ -689,7 +742,7 @@ describe('Controllers', function () { }); it('should return 503 in maintenance mode', function (done) { - request(nconf.get('url') + '/recent', { json: true }, function (err, res, body) { + request(nconf.get('url') + '/recent', { json: true }, function (err, res) { assert.ifError(err); assert.equal(res.statusCode, 503); done(); diff --git a/test/meta.js b/test/meta.js index 9d1a69b00c..4544570b42 100644 --- a/test/meta.js +++ b/test/meta.js @@ -123,6 +123,17 @@ describe('meta', function () { }); }); + it('should set single config value', function (done) { + socketAdmin.config.set({ uid: fooUid }, { key: 'someKey', value: 'someValue' }, function (err) { + assert.ifError(err); + meta.configs.getFields(['someKey'], function (err, data) { + assert.ifError(err); + assert.equal(data.someKey, 'someValue'); + done(); + }); + }); + }); + it('should set config value', function (done) { meta.configs.set('someField', 'someValue', function (err) { assert.ifError(err); diff --git a/test/socket.io.js b/test/socket.io.js index 6c935657ac..c534d0969c 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -16,6 +16,9 @@ var user = require('../src/user'); var groups = require('../src/groups'); var categories = require('../src/categories'); var helpers = require('./helpers'); +var meta = require('../src/meta'); + +var socketAdmin = require('../src/socket.io/admin'); describe('socket.io', function () { var io; @@ -156,7 +159,6 @@ describe('socket.io', function () { }); it('should make user admin', function (done) { - var socketAdmin = require('../src/socket.io/admin'); socketAdmin.user.makeAdmins({ uid: adminUid }, [regularUid], function (err) { assert.ifError(err); groups.isMember(regularUid, 'administrators', function (err, isMember) { @@ -168,7 +170,6 @@ describe('socket.io', function () { }); it('should make user non-admin', function (done) { - var socketAdmin = require('../src/socket.io/admin'); socketAdmin.user.removeAdmins({ uid: adminUid }, [regularUid], function (err) { assert.ifError(err); groups.isMember(regularUid, 'administrators', function (err, isMember) { @@ -180,7 +181,6 @@ describe('socket.io', function () { }); describe('create/delete', function () { - var socketAdmin = require('../src/socket.io/admin'); var uid; it('should create a user', function (done) { socketAdmin.user.createUser({ uid: adminUid }, { username: 'foo1' }, function (err, _uid) { @@ -214,7 +214,6 @@ describe('socket.io', function () { }); it('should error with invalid data', function (done) { - var socketAdmin = require('../src/socket.io/admin'); socketAdmin.user.createUser({ uid: adminUid }, null, function (err) { assert.equal(err.message, '[[error:invalid-data]]'); done(); @@ -222,7 +221,6 @@ describe('socket.io', function () { }); it('should reset lockouts', function (done) { - var socketAdmin = require('../src/socket.io/admin'); socketAdmin.user.resetLockouts({ uid: adminUid }, [regularUid], function (err) { assert.ifError(err); done(); @@ -230,7 +228,6 @@ describe('socket.io', function () { }); describe('validation emails', function () { - var socketAdmin = require('../src/socket.io/admin'); var meta = require('../src/meta'); it('should validate emails', function (done) { @@ -245,7 +242,6 @@ describe('socket.io', function () { }); it('should error with invalid uids', function (done) { - var socketAdmin = require('../src/socket.io/admin'); socketAdmin.user.sendValidationEmail({ uid: adminUid }, null, function (err) { assert.equal(err.message, '[[error:invalid-data]]'); done(); @@ -253,7 +249,6 @@ describe('socket.io', function () { }); it('should error if email validation is not required', function (done) { - var socketAdmin = require('../src/socket.io/admin'); socketAdmin.user.sendValidationEmail({ uid: adminUid }, [regularUid], function (err) { assert.equal(err.message, '[[error:email-confirmations-are-disabled]]'); done(); @@ -261,7 +256,6 @@ describe('socket.io', function () { }); it('should send validation email', function (done) { - var socketAdmin = require('../src/socket.io/admin'); meta.config.requireEmailConfirmation = 1; socketAdmin.user.sendValidationEmail({ uid: adminUid }, [regularUid], function (err) { assert.ifError(err); @@ -272,7 +266,6 @@ describe('socket.io', function () { }); it('should search users', function (done) { - var socketAdmin = require('../src/socket.io/admin'); socketAdmin.user.search({ uid: adminUid }, { query: 'reg', searchBy: 'username' }, function (err, data) { assert.ifError(err); assert.equal(data.matchCount, 1); @@ -328,6 +321,13 @@ describe('socket.io', function () { }); }); + it('should error to get daily analytics with invalid data', function (done) { + io.emit('admin.analytics.get', null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + it('should get daily analytics', function (done) { io.emit('admin.analytics.get', { graph: 'traffic', units: 'days' }, function (err, data) { assert.ifError(err); @@ -347,7 +347,6 @@ describe('socket.io', function () { }); it('should return error', function (done) { - var socketAdmin = require('../src/socket.io/admin'); socketAdmin.before({ uid: 10 }, 'someMethod', {}, function (err) { assert.equal(err.message, '[[error:no-privileges]]'); done(); @@ -355,8 +354,6 @@ describe('socket.io', function () { }); it('should get room stats', function (done) { - var socketAdmin = require('../src/socket.io/admin'); - io.emit('meta.rooms.enter', { enter: 'topic_1' }, function (err) { assert.ifError(err); socketAdmin.rooms.getAll({ uid: 10 }, {}, function (err) { @@ -378,8 +375,6 @@ describe('socket.io', function () { }); it('should get room stats', function (done) { - var socketAdmin = require('../src/socket.io/admin'); - io.emit('meta.rooms.enter', { enter: 'category_1' }, function (err) { assert.ifError(err); socketAdmin.rooms.getAll({ uid: 10 }, {}, function (err) { @@ -396,7 +391,6 @@ describe('socket.io', function () { }); it('should get admin search dictionary', function (done) { - var socketAdmin = require('../src/socket.io/admin'); socketAdmin.getSearchDict({ uid: adminUid }, {}, function (err, data) { assert.ifError(err); assert(Array.isArray(data)); @@ -407,6 +401,173 @@ describe('socket.io', function () { }); }); + it('should fire event', function (done) { + io.on('testEvent', function (data) { + assert.equal(data.foo, 1); + done(); + }); + socketAdmin.fireEvent({ uid: adminUid }, { name: 'testEvent', payload: { foo: 1 } }, function (err) { + assert.ifError(err); + }); + }); + + it('should error with invalid data', function (done) { + socketAdmin.themes.set({ uid: adminUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + + it('should set theme to bootswatch', function (done) { + socketAdmin.themes.set({ uid: adminUid }, { type: 'bootswatch', src: 'darkly' }, function (err) { + assert.ifError(err); + meta.configs.get('theme:src', function (err, id) { + assert.ifError(err); + assert.equal(id, 'darkly'); + done(); + }); + }); + }); + + it('should set theme to local persona', function (done) { + socketAdmin.themes.set({ uid: adminUid }, { type: 'local', id: 'nodebb-theme-persona' }, function (err) { + assert.ifError(err); + meta.configs.get('theme:id', function (err, id) { + assert.ifError(err); + assert.equal(id, 'nodebb-theme-persona'); + done(); + }); + }); + }); + + it('should toggle plugin active', function (done) { + socketAdmin.plugins.toggleActive({ uid: adminUid }, 'nodebb-plugin-location-to-map', function (err, data) { + assert.ifError(err); + assert.deepEqual(data, { id: 'nodebb-plugin-location-to-map', active: true }); + done(); + }); + }); + + it('should toggle plugin install', function (done) { + socketAdmin.plugins.toggleInstall({ uid: adminUid }, { id: 'nodebb-plugin-location-to-map', version: 'latest' }, function (err, data) { + assert.ifError(err); + assert.equal(data.name, 'nodebb-plugin-location-to-map'); + done(); + }); + }); + + it('should get list of active plugins', function (done) { + socketAdmin.plugins.getActive({ uid: adminUid }, {}, function (err, data) { + assert.ifError(err); + assert(Array.isArray(data)); + done(); + }); + }); + + it('should order active plugins', function (done) { + var data = [ + { name: 'nodebb-theme-persona', order: 0 }, + { name: 'nodebb-plugin-dbsearch', order: 1 }, + { ignoreme: 'wrong data' }, + ]; + socketAdmin.plugins.orderActivePlugins({ uid: adminUid }, data, function (err) { + assert.ifError(err); + db.sortedSetRank('plugins:active', 'nodebb-plugin-dbsearch', function (err, rank) { + assert.ifError(err); + assert.equal(rank, 1); + done(); + }); + }); + }); + + it('should upgrade plugin', function (done) { + socketAdmin.plugins.upgrade({ uid: adminUid }, { id: 'nodebb-plugin-location-to-map', version: 'latest' }, function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should error with invalid data', function (done) { + socketAdmin.widgets.set({ uid: adminUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + + it('should error with invalid data', function (done) { + var data = { template: 'global', location: 'sidebar', widgets: [{ widget: 'html', data: { html: 'test', title: 'test', container: '' } }] }; + socketAdmin.widgets.set({ uid: adminUid }, data, function (err) { + assert.ifError(err); + db.getObjectField('widgets:global', 'sidebar', function (err, widgetData) { + assert.ifError(err); + + assert.equal(JSON.parse(widgetData)[0].data.html, 'test'); + done(); + }); + }); + }); + + it('should clear sitemap cache', function (done) { + socketAdmin.settings.clearSitemapCache({ uid: adminUid }, {}, function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should send test email', function (done) { + socketAdmin.email.test({ uid: adminUid }, { template: 'digest.tpl' }, function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should get logs', function (done) { + var fs = require('fs'); + var path = require('path'); + meta.logs.path = path.join(nconf.get('base_dir'), 'test/files', 'output.log'); + fs.appendFile(meta.logs.path, 'some logs', function (err) { + assert.ifError(err); + + socketAdmin.logs.get({ uid: adminUid }, {}, function (err, data) { + assert.ifError(err); + assert(data); + done(); + }); + }); + }); + + it('should clear logs', function (done) { + socketAdmin.logs.clear({ uid: adminUid }, {}, function (err) { + assert.ifError(err); + socketAdmin.logs.get({ uid: adminUid }, {}, function (err, data) { + assert.ifError(err); + assert.equal(data.length, 0); + done(); + }); + }); + }); + + it('should clear errors', function (done) { + socketAdmin.errors.clear({ uid: adminUid }, {}, function (err) { + assert.ifError(err); + db.exists('error:404', function (err, exists) { + assert.ifError(err); + assert(!exists); + done(); + }); + }); + }); + + it('shoudl delete all events', function (done) { + socketAdmin.deleteAllEvents({ uid: adminUid }, {}, function (err) { + assert.ifError(err); + db.sortedSetCard('events:time', function (err, count) { + assert.ifError(err); + assert.equal(count, 0); + done(); + }); + }); + }); after(function (done) { db.emptydb(done);