diff --git a/nodebb b/nodebb index ded766b51e..880afdf563 100755 --- a/nodebb +++ b/nodebb @@ -20,7 +20,7 @@ case "$1" in echo "Launching NodeBB in \"development\" mode." echo "To run the production build of NodeBB, please use \"forever\"." echo "More Information: https://github.com/designcreateplay/NodeBB/wiki/How-to-run-NodeBB" - NODE_ENV=development supervisor --extensions 'node|js|tpl' -- app $1 + NODE_ENV=development supervisor -q --extensions 'node|js|tpl' -- app $1 ;; language) diff --git a/public/language/en/user.json b/public/language/en/user.json index 1b58f198de..caa4a59b57 100644 --- a/public/language/en/user.json +++ b/public/language/en/user.json @@ -32,5 +32,8 @@ "show_email": "Show My Email", "has_no_follower": "This user doesn't have any followers :(", - "follows_no_one": "This user isn't following anyone :(" + "follows_no_one": "This user isn't following anyone :(", + + "email_hidden": "Email Hidden", + "hidden": "hidden" } diff --git a/public/src/forum/admin/categories.js b/public/src/forum/admin/categories.js index 0874ffa878..73eafde5df 100644 --- a/public/src/forum/admin/categories.js +++ b/public/src/forum/admin/categories.js @@ -211,6 +211,7 @@ define(['uploader'], function(uploader) { var modal = $('#category-permissions-modal'), searchEl = modal.find('#permission-search'), resultsEl = modal.find('.search-results'), + groupsResultsEl = modal.find('.groups-results'), searchDelay; searchEl.off().on('keyup', function() { @@ -263,6 +264,40 @@ define(['uploader'], function(uploader) { searchEl.keyup(); }); + // User Groups and privileges + socket.emit('api:admin.categories.groupsearch', cid, function(err, results) { + var groupsFrag = document.createDocumentFragment(), + liEl = document.createElement('li'); + var numResults = results.length, + resultObj; + + for(var x=0;x' + + '' + + '' + + '' + + '' + + ' '+resultObj.name; + + groupsFrag.appendChild(liEl.cloneNode(true)); + } + + groupsResultsEl.html(groupsFrag); + }); + + groupsResultsEl.off().on('click', '[data-gpriv]', function(e) { + var btnEl = $(this), + gid = btnEl.parents('li[data-gid]').attr('data-gid'), + privilege = this.getAttribute('data-gpriv'); + e.preventDefault(); + socket.emit('api:admin.categories.setGroupPrivilege', cid, gid, privilege, !btnEl.hasClass('active'), function(err, privileges) { + btnEl.toggleClass('active', privileges[privilege]); + }); + }) + modal.modal(); }; @@ -312,4 +347,4 @@ define(['uploader'], function(uploader) { }; return Categories; -}); \ No newline at end of file +}); diff --git a/public/src/forum/topic.js b/public/src/forum/topic.js index 3a47ce901c..424d12db2c 100644 --- a/public/src/forum/topic.js +++ b/public/src/forum/topic.js @@ -102,7 +102,7 @@ define(function() { var loadingEl = document.getElementById('categories-loading'); if (loadingEl) { - socket.once('api:categories.get', function(data) { + socket.emit('api:categories.get', function(data) { // Render categories var categoriesFrag = document.createDocumentFragment(), categoryEl = document.createElement('li'), @@ -172,7 +172,6 @@ define(function() { } }); }); - socket.emit('api:categories.get'); } }); } @@ -483,7 +482,21 @@ define(function() { adjust_rep(-1, data.pid, data.uid); }); - socket.on('event:new_post', createNewPosts); + socket.on('event:new_post', function(data) { + var posts = data.posts; + for (var p in posts) { + if (posts.hasOwnProperty(p)) { + var post = posts[p], + postcount = jQuery('.user_postcount_' + post.uid), + ptotal = parseInt(postcount.html(), 10); + + ptotal += 1; + postcount.html(ptotal); + } + } + + createNewPosts(data); + }); socket.on('event:topic_deleted', function(data) { if (data.tid === tid && data.status === 'ok') { @@ -802,7 +815,7 @@ define(function() { pagination.parentNode.style.display = 'block'; progressBarContainer.css('display', ''); - + if (scrollTop < jQuery('.posts > .post-row:first-child').height() && Topic.postCount > 1) { localStorage.removeItem("topic:" + tid + ":bookmark"); pagination.innerHTML = '1 out of ' + Topic.postCount; diff --git a/public/templates/account.tpl b/public/templates/account.tpl index 200fd854c2..571bb2ddd2 100644 --- a/public/templates/account.tpl +++ b/public/templates/account.tpl @@ -20,34 +20,51 @@ [[user:banned]] +
+ Chat +
- Follow - Unfollow + Follow + Unfollow
diff --git a/public/templates/admin/categories.tpl b/public/templates/admin/categories.tpl index a8da120e82..9651b387a9 100644 --- a/public/templates/admin/categories.tpl +++ b/public/templates/admin/categories.tpl @@ -154,12 +154,18 @@ +
+
+ +
+
+ - \ No newline at end of file + diff --git a/public/templates/login.tpl b/public/templates/login.tpl index cad99bc5a4..233986f339 100644 --- a/public/templates/login.tpl +++ b/public/templates/login.tpl @@ -40,7 +40,8 @@
-   [[login:forgot_password]] +
+   [[login:forgot_password]]
diff --git a/public/templates/search.tpl b/public/templates/search.tpl index 9bea91611c..0f2ad27a30 100644 --- a/public/templates/search.tpl +++ b/public/templates/search.tpl @@ -15,41 +15,54 @@ diff --git a/public/templates/topic.tpl b/public/templates/topic.tpl index 7802c41524..a09ee029e5 100644 --- a/public/templates/topic.tpl +++ b/public/templates/topic.tpl @@ -109,7 +109,7 @@
- [[topic:reputation]]: {posts.user_rep} | [[topic:posts]]: {posts.user_postcount} + [[topic:reputation]]: {posts.user_rep} | [[topic:posts]]: {posts.user_postcount} {posts.additional_profile_info} diff --git a/src/categoryTools.js b/src/categoryTools.js index ccbce0f4fd..e1f79e8c07 100644 --- a/src/categoryTools.js +++ b/src/categoryTools.js @@ -65,4 +65,56 @@ CategoryTools.privileges = function(cid, uid, callback) { }); }; -module.exports = CategoryTools; \ No newline at end of file +CategoryTools.groupPrivileges = function(cid, gid, callback) { + async.parallel({ + "+gr": function(next) { + var key = 'cid:' + cid + ':privileges:+gr'; + Groups.exists(key, function(err, exists) { + if (exists) { + async.parallel({ + isMember: function(next) { + Groups.isMemberByGroupName(gid, key, next); + }, + isEmpty: function(next) { + Groups.isEmptyByGroupName(key, next); + } + }, next); + } else { + next(null, { + isMember: false, + isEmpty: true + }); + } + }); + }, + "+gw": function(next) { + var key = 'cid:' + cid + ':privileges:+gw'; + Groups.exists(key, function(err, exists) { + if (exists) { + async.parallel({ + isMember: function(next) { + Groups.isMemberByGroupName(gid, key, next); + }, + isEmpty: function(next) { + Groups.isEmptyByGroupName(key, next); + } + }, next); + } else { + next(null, { + isMember: false, + isEmpty: true + }); + } + }); + } + }, function(err, privileges) { + callback(err, !privileges ? null : { + "+gr": privileges['+gr'].isMember, + "+gw": privileges['+gw'].isMember, + read: (privileges['+gr'].isMember || privileges['+gr'].isEmpty), + write: (privileges['+gw'].isMember || privileges['+gw'].isEmpty), + }); + }); +}; + +module.exports = CategoryTools; diff --git a/src/groups.js b/src/groups.js index 3d56520257..dc235fd26b 100644 --- a/src/groups.js +++ b/src/groups.js @@ -176,7 +176,6 @@ Groups.update = function(gid, values, callback) { db.exists('gid:' + gid, function (err, exists) { - console.log('exists?', gid, exists, values); if (!err && exists) { db.setObject('gid:' + gid, values, callback); } else { @@ -199,7 +198,6 @@ Groups.getGidFromName(groupName, function(err, gid) { if (err || !gid) { Groups.create(groupName, '', function(err, groupObj) { - console.log('creating group, calling hide', groupObj.gid); async.parallel([ function(next) { Groups.hide(groupObj.gid, next); @@ -263,4 +261,40 @@ }); }; + Groups.getCategoryAccess = function(cid, uid, callback){ + var access = false; + // check user group read access level + async.series([function(callback){ + // get groups with read permission + db.getObjectField('group:gid', 'cid:' + cid + ':privileges:+gr', function(err, gid){ + // get the user groups that belong to this read group + db.getSetMembers('gid:' + gid + ':members', function (err, gids) { + // check if user belong to any of these user groups + var groups_check = new Array(); + gids.forEach(function(cgid){ + groups_check.push(function(callback){ + Groups.isMember(uid, cgid, function(err, isMember){ + if (isMember){ + access = true; + } + callback(null, gids); + }) + }); + }); + // do a series check. We want to make sure we check all the groups before determining if the user + // has access or not. + async.series(groups_check, function(err, results){ + callback(null, results); + }); + }); + }); + + }], + function(err, results){ + // if the read group is empty we will asume that read access has been granted to ALL + if (results[0].length == 0){ access = true; } + callback(false, access); + }); + }; + }(module.exports)); diff --git a/src/posts.js b/src/posts.js index fd250d5b38..74c9e2fb0f 100644 --- a/src/posts.js +++ b/src/posts.js @@ -14,7 +14,8 @@ var db = require('./database'), async = require('async'), nconf = require('nconf'), validator = require('validator'), - winston = require('winston'); + winston = require('winston'), + gravatar = require('gravatar'); (function(Posts) { var customUserInfo = {}; @@ -202,12 +203,16 @@ var db = require('./database'), } postTools.parseSignature(userData.signature, function(err, signature) { + if(err) { + return callback(err); + } + post.username = userData.username || 'anonymous'; post.userslug = userData.userslug || ''; post.user_rep = userData.reputation || 0; post.user_postcount = userData.postcount || 0; post.user_banned = parseInt(userData.banned, 10) === 1; - post.picture = userData.picture || require('gravatar').url('', {}, https = nconf.get('https')); + post.picture = userData.picture || gravatar.url('', {}, https = nconf.get('https')); post.signature = signature; for (var info in customUserInfo) { @@ -217,11 +222,16 @@ var db = require('./database'), } plugins.fireHook('filter:posts.custom_profile_info', {profile: "", uid: post.uid, pid: post.pid}, function(err, profile_info) { + if(err) { + return callback(err); + } post.additional_profile_info = profile_info.profile; if (post.editor !== '') { user.getUserFields(post.editor, ['username', 'userslug'], function(err, editorData) { - if (err) return callback(); + if (err) { + return callback(err); + } post.editorname = editorData.username; post.editorslug = editorData.userslug; @@ -439,8 +449,9 @@ var db = require('./database'), Posts.uploadPostImage = function(image, callback) { - if(!image) + if(!image) { return callback('invalid image', null); + } require('./imgur').upload(meta.config.imgurClientID, image.data, 'base64', function(err, data) { if(err) { @@ -455,22 +466,29 @@ var db = require('./database'), } Posts.getPostsByUid = function(uid, start, end, callback) { - user.getPostIds(uid, start, end, function(pids) { + user.getPostIds(uid, start, end, function(err, pids) { + if(err) { + return callback(err); + } if (pids && pids.length) { plugins.fireHook('filter:post.getTopic', pids, function(err, posts) { + if(err) { + return callback(err); + } - if (!err & 0 < posts.length) { + if (posts && posts.length) { Posts.getPostsByPids(pids, function(err, posts) { plugins.fireHook('action:post.gotTopic', posts); - callback(posts); + callback(null, posts); }); } else { - callback(posts); + callback(null, []); } }); - } else - callback([]); + } else { + callback(null, []); + } }); } diff --git a/src/routes/admin.js b/src/routes/admin.js index cd3994f144..3bb8783340 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -244,7 +244,7 @@ var nconf = require('nconf'), var custom_routes = { 'routes': [], - 'api_methods': [] + 'api': [] }; plugins.ready(function() { @@ -264,6 +264,19 @@ var nconf = require('nconf'), }(route)); } } + + var apiRoutes = custom_routes.api; + for (var route in apiRoutes) { + if (apiRoutes.hasOwnProperty(route)) { + (function(route) { + app[apiRoutes[route].method || 'get']('/api/admin' + apiRoutes[route].route, function(req, res) { + apiRoutes[route].callback(req, res, function(data) { + res.json(data); + }); + }); + }(route)); + } + } }); }); diff --git a/src/routes/api.js b/src/routes/api.js index ca0955ccbe..cb28e0bffb 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -4,6 +4,7 @@ var path = require('path'), db = require('../database'), user = require('../user'), + groups = require('../groups'), auth = require('./authentication'), topics = require('../topics'), posts = require('../posts'), @@ -39,33 +40,26 @@ var path = require('path'), res.json(200, config); }); - app.get('/home', function (req, res, next) { + app.get('/home', function (req, res) { var uid = (req.user) ? req.user.uid : 0; categories.getAllCategories(uid, function (err, data) { data.categories = data.categories.filter(function (category) { return (!category.disabled || parseInt(category.disabled, 10) === 0); }); - function getRecentReplies(category, callback) { + function iterator(category, callback) { categories.getRecentReplies(category.cid, 2, function (err, posts) { - if(err) { - return callback(err); - } category.posts = posts; category.post_count = posts.length > 2 ? 2 : posts.length; callback(null); }); } - async.each(data.categories, getRecentReplies, function (err) { - if(err) { - return next(err); - } - + async.each(data.categories, iterator, function (err) { data.motd_class = (parseInt(meta.config.show_motd, 10) === 1 || meta.config.show_motd === undefined) ? '' : ' none'; data.motd_class += (meta.config.motd && meta.config.motd.length > 0 ? '' : ' default'); - data.motd = require('marked')(meta.config.motd || "\n\n# NodeBB v" + pkg.version + "\nWelcome to NodeBB, the discussion platform of the future."); + data.motd = require('marked')(meta.config.motd || "\n\n# NodeBB v" + pkg.version + "\nWelcome to NodeBB, the discussion platform of the future."); res.json(data); }); }); @@ -128,7 +122,15 @@ var path = require('path'), if (parseInt(data.deleted, 10) === 1 && parseInt(data.expose_tools, 10) === 0) { return res.json(404, {}); } - res.json(data); + // get the category this post belongs to and check category access + var cid = data.category_slug.split("/")[0]; + groups.getCategoryAccess(cid, uid, function(err, access){ + if (access){ + res.json(data); + } else { + res.send(403); + } + }) } else next(); }); }); @@ -139,13 +141,20 @@ var path = require('path'), // Category Whitelisting categoryTools.privileges(req.params.id, uid, function(err, privileges) { if (!err && privileges.read) { - categories.getCategoryById(req.params.id, uid, function (err, data) { - if (!err && data && parseInt(data.disabled, 10) === 0) { - res.json(data); + groups.getCategoryAccess(req.params.id, uid, function(err, access){ + if (access){ + categories.getCategoryById(req.params.id, uid, function (err, data) { + if (!err && data && parseInt(data.disabled, 10) === 0) { + res.json(data); + } else { + next(); + } + }, req.params.id, uid); } else { - next(); + res.send(403); } - }, req.params.id, uid); + + }); } else { res.send(403); } @@ -244,10 +253,6 @@ var path = require('path'), return callback(err, null); } - if(pids.length > 50) { - pids = pids.splice(0, 50); - } - posts.getPostSummaryByPids(pids, false, function (err, posts) { if (err){ return callback(err, null); @@ -263,10 +268,6 @@ var path = require('path'), return callback(err, null); } - if(tids.length > 50) { - tids = tids.splice(0, 50); - } - topics.getTopicsByTids(tids, 0, function (topics) { callback(null, topics); }, 0); @@ -285,7 +286,9 @@ var path = require('path'), show_results: '', search_query: req.params.term, posts: results[0], - topics: results[1] + topics: results[1], + post_matches : results[0].length, + topic_matches : results[1].length }); }); } else { @@ -313,7 +316,7 @@ var path = require('path'), app.get('/500', function(req, res) { res.json({errorMessage: 'testing'}); - }) + }); }); } -}(exports)); \ No newline at end of file +}(exports)); diff --git a/src/routes/authentication.js b/src/routes/authentication.js index 743ad9f9b0..5b4291d45d 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -72,7 +72,7 @@ login_strategies.push({ name: 'google', url: '/auth/google', - callbackURL: nconf.get('url') + '/auth/google/callback', + callbackURL: '/auth/google/callback', icon: 'google-plus', scope: 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email' }); @@ -95,7 +95,7 @@ login_strategies.push({ name: 'facebook', url: '/auth/facebook', - callbackURL: nconf.get('url') + '/auth/facebook/callback', + callbackURL: '/auth/facebook/callback', icon: 'facebook', scope: 'email' }); diff --git a/src/routes/user.js b/src/routes/user.js index a978e50eee..d7b88ff03e 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -181,7 +181,7 @@ var fs = require('fs'), user.setUserField(uid, 'picture', imageUrl); if (convertToPNG) { - im.convert([uploadPath, 'png:-'], + im.convert([uploadPath, 'png:-'], function(err, stdout){ if (err) { winston.err(err); @@ -191,10 +191,10 @@ var fs = require('fs'), return; } - fs.writeFileSync(uploadPath, stdout, 'binary'); + fs.writeFileSync(uploadPath, stdout, 'binary'); }); } - + res.json({ path: imageUrl @@ -393,18 +393,24 @@ var fs = require('fs'), }); }); - app.get('/api/user/:userslug', function (req, res) { + app.get('/api/user/:userslug', function (req, res, next) { var callerUID = req.user ? req.user.uid : '0'; getUserDataByUserSlug(req.params.userslug, callerUID, function (userData) { if (userData) { user.isFollowing(callerUID, userData.theirid, function (isFollowing) { - posts.getPostsByUid(userData.theirid, 0, 9, function (posts) { + posts.getPostsByUid(userData.theirid, 0, 9, function (err, posts) { + + if(err) { + return next(err); + } userData.posts = posts.filter(function (p) { return p && parseInt(p.deleted, 10) !== 1; }); + userData.isFollowing = isFollowing; + if (!userData.profileviews) { userData.profileviews = 1; } diff --git a/src/user.js b/src/user.js index bfd8f47826..fd6a6eea31 100644 --- a/src/user.js +++ b/src/user.js @@ -444,21 +444,18 @@ var bcrypt = require('bcrypt'), User.getPostIds = function(uid, start, stop, callback) { db.getListRange('uid:' + uid + ':posts', start, stop, function(err, pids) { - if (!err) { - if (pids && pids.length) { - callback(pids); - } else { - callback([]); - } + if(err) { + return callback(err); + } + + if (pids && pids.length) { + callback(null, pids); } else { - console.log(err); - callback([]); + callback(null, []); } }); }; - - User.follow = function(uid, followid, callback) { db.setAdd('following:' + uid, followid, function(err, data) { if (!err) { diff --git a/src/websockets.js b/src/websockets.js index b7ccb81ee2..2000b34617 100644 --- a/src/websockets.js +++ b/src/websockets.js @@ -591,9 +591,9 @@ websockets.init = function(io) { threadTools.move(data.tid, data.cid, socket); }); - socket.on('api:categories.get', function() { + socket.on('api:categories.get', function(callback) { categories.getAllCategories(0, function(err, categories) { - socket.emit('api:categories.get', categories); + callback(categories); }); }); @@ -714,10 +714,10 @@ websockets.init = function(io) { return; } - var finalMessage = username + ' : ' + msg, - notifText = 'New message from ' + username + '', - username = usersData[0].username, - toUsername = usersData[1].username; + var username = usersData[0].username, + toUsername = usersData[1].username, + finalMessage = username + ' : ' + msg, + notifText = 'New message from ' + username + ''; if (!isUserOnline(touid)) { notifications.create(notifText, 'javascript:app.openChat('' + username + '', ' + uid + ');', 'notification_' + uid + '_' + touid, function(nid) { @@ -1084,6 +1084,37 @@ websockets.init = function(io) { }); }); + socket.on('api:admin.categories.setGroupPrivilege', function(cid, gid, privilege, set, callback) { + var cb = function(err) { + CategoryTools.groupPrivileges(cid, gid, callback); + }; + + if (set) { + groups.joinByGroupName('cid:' + cid + ':privileges:' + privilege, gid, cb); + } else { + groups.leaveByGroupName('cid:' + cid + ':privileges:' + privilege, gid, cb); + } + }); + + socket.on('api:admin.categories.groupsearch', function(cid, callback) { + groups.list({expand:false}, function(err, data){ + async.map(data, function(groupObj, next) { + CategoryTools.groupPrivileges(cid, groupObj.gid, function(err, privileges) { + if (!err) { + groupObj.privileges = privileges; + } else { + winston.error('[socket api:admin.categories.groupsearch] Could not retrieve permissions'); + } + + next(null, groupObj); + }); + }, function(err, data) { + if (!callback) socket.emit('api:admin.categories.groupsearch', data); + else callback(null, data); + }); + }); + }); + socket.on('api:admin.themes.getInstalled', function(callback) { meta.themes.get(function(err, themeArr) { callback(themeArr);