diff --git a/package.json b/package.json index 8164ee178d..add7cde955 100644 --- a/package.json +++ b/package.json @@ -59,10 +59,10 @@ "nodebb-plugin-markdown": "7.1.1", "nodebb-plugin-mentions": "2.0.1", "nodebb-plugin-soundpack-default": "1.0.0", - "nodebb-plugin-spam-be-gone": "0.4.10", + "nodebb-plugin-spam-be-gone": "0.4.13", "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "3.0.15", - "nodebb-theme-persona": "4.2.4", + "nodebb-theme-persona": "4.2.6", "nodebb-theme-vanilla": "5.2.0", "nodebb-widget-essentials": "2.0.13", "nodemailer": "2.6.4", diff --git a/public/language/en-GB/admin/settings/general.json b/public/language/en-GB/admin/settings/general.json index 72ecfe641f..3f2814bd88 100644 --- a/public/language/en-GB/admin/settings/general.json +++ b/public/language/en-GB/admin/settings/general.json @@ -27,5 +27,6 @@ "touch-icon.help": "Recommended size and format: 192x192, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", - "search-default-sort-by": "Search default sort by" + "search-default-sort-by": "Search default sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page" } \ No newline at end of file diff --git a/public/language/en-GB/groups.json b/public/language/en-GB/groups.json index a55cc8603f..08c8d4d1f5 100644 --- a/public/language/en-GB/groups.json +++ b/public/language/en-GB/groups.json @@ -32,6 +32,7 @@ "details.disableJoinRequests": "Disable join requests", "details.grant": "Grant/Rescind Ownership", "details.kick": "Kick", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", "details.owner_options": "Group Administration", "details.group_name": "Group Name", diff --git a/public/language/pl/admin/settings/group.json b/public/language/pl/admin/settings/group.json index 6db8cb32b4..c5900c2a39 100644 --- a/public/language/pl/admin/settings/group.json +++ b/public/language/pl/admin/settings/group.json @@ -1,5 +1,5 @@ { - "general": "General", + "general": "Ogólne", "private-groups": "Prywatne Grupy", "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", diff --git a/public/language/pl/admin/settings/tags.json b/public/language/pl/admin/settings/tags.json index d67523c8e6..ef8efabbcd 100644 --- a/public/language/pl/admin/settings/tags.json +++ b/public/language/pl/admin/settings/tags.json @@ -1,6 +1,6 @@ { "tag": "Ustawienia Tagów", - "min-per-topic": "Minimum Tags per Topic", + "min-per-topic": "Minimalna ilość Tagów na Temat", "max-per-topic": "Maximum Tags per Topic", "min-length": "Minimum Tag Length", "max-length": "Maximum Tag Length", diff --git a/public/language/pl/admin/settings/uploads.json b/public/language/pl/admin/settings/uploads.json index 9a610fb576..05edbff41a 100644 --- a/public/language/pl/admin/settings/uploads.json +++ b/public/language/pl/admin/settings/uploads.json @@ -1,6 +1,6 @@ { "posts": "Posty", - "allow-files": "Allow users to upload regular files", + "allow-files": "Pozwolić użytkownikom wgrywać pliki", "private": "Make uploaded files private", "max-image-width": "Resize images down to specified width (in pixels)", "max-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less index 23f3bead8c..bcf6ddca58 100644 --- a/public/less/admin/admin.less +++ b/public/less/admin/admin.less @@ -272,4 +272,8 @@ body { border: 1px dashed @brand-success; background: lighten(@brand-success, 10%); opacity: 0.5; +} + +form small { + color: @gray-light; } \ No newline at end of file diff --git a/public/less/admin/settings.less b/public/less/admin/settings.less index 84572def68..decfc8b7ed 100644 --- a/public/less/admin/settings.less +++ b/public/less/admin/settings.less @@ -16,4 +16,20 @@ [data-action="upload"][type="text"] { width: 95%; } + + .bootstrap-tagsinput { + width: 100%; + border: 0; + box-shadow: none; + padding-left: 0; + + input { + width: 100%; + margin-left: 1px; + margin-top: 9px; + border-bottom: 1px dotted #ccc !important; + padding-bottom: 5px; + padding-left: 0; + } + } } \ No newline at end of file diff --git a/public/src/admin/settings.js b/public/src/admin/settings.js index 041de4a40f..5495e3b71b 100644 --- a/public/src/admin/settings.js +++ b/public/src/admin/settings.js @@ -102,6 +102,7 @@ define('admin/settings', ['uploader'], function (uploader) { }); handleUploads(); + setupTagsInput(); $('#clear-sitemap-cache').off('click').on('click', function () { socket.emit('admin.settings.clearSitemapCache', function () { @@ -142,6 +143,14 @@ define('admin/settings', ['uploader'], function (uploader) { }); } + function setupTagsInput() { + $('[data-field-type="tagsinput"]').tagsinput({ + confirmKeys: [13, 44], + trimValue: true, + }); + app.flags._unsaved = false; + } + Settings.remove = function (key) { socket.emit('admin.config.remove', key); }; diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index 913dbb6fd6..93eb4791b0 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -366,8 +366,13 @@ $(document).ready(function () { window.open(this.href, '_blank'); e.preventDefault(); } else if (config.useOutgoingLinksPage) { - ajaxify.go('outgoing?url=' + encodeURIComponent(this.href)); - e.preventDefault(); + var safeUrls = config.outgoingLinksWhitelist.trim().split(/[\s,]+/g); + var href = this.href; + + if (!safeUrls.some(function (url) { return href.indexOf(url) !== -1; })) { + ajaxify.go('outgoing?url=' + encodeURIComponent(href)); + e.preventDefault(); + } } } } diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index 4f7a11a892..e7225bb16c 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -75,15 +75,23 @@ define('forum/groups/details', [ break; case 'kick': - socket.emit('groups.kick', { - uid: uid, - groupName: groupName, - }, function (err) { - if (!err) { - userRow.slideUp().remove(); - } else { - app.alertError(err.message); - } + translator.translate('[[groups:details.kick_confirm]]', function (translated) { + bootbox.confirm(translated, function (confirm) { + if (!confirm) { + return; + } + + socket.emit('groups.kick', { + uid: uid, + groupName: groupName, + }, function (err) { + if (!err) { + userRow.slideUp().remove(); + } else { + app.alertError(err.message); + } + }); + }); }); break; diff --git a/src/controllers/api.js b/src/controllers/api.js index 5ddbd0731b..f7158481e7 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -64,6 +64,10 @@ apiController.getConfig = function (req, res, next) { config.bootswatchSkin = meta.config.bootswatchSkin || 'noskin'; config.defaultBootswatchSkin = meta.config.bootswatchSkin || 'noskin'; + if (config.useOutgoingLinksPage) { + config.outgoingLinksWhitelist = meta.config['outgoingLinks:whitelist']; + } + var timeagoCutoff = meta.config.timeagoCutoff === undefined ? 30 : meta.config.timeagoCutoff; config.timeagoCutoff = timeagoCutoff !== '' ? Math.max(0, parseInt(timeagoCutoff, 10)) : timeagoCutoff; diff --git a/src/database/mongo.js b/src/database/mongo.js index 9bc321703b..8e6494ffd7 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -47,13 +47,8 @@ module.init = function (callback) { callback = callback || function () { }; - var mongoClient; - try { - mongoClient = require('mongodb').MongoClient; - } catch (err) { - winston.error('Unable to initialize MongoDB! Is MongoDB installed? Error :' + err.message); - return callback(err); - } + + var mongoClient = require('mongodb').MongoClient; var usernamePassword = ''; if (nconf.get('mongo:username') && nconf.get('mongo:password')) { @@ -84,10 +79,13 @@ var connOptions = { server: { poolSize: parseInt(nconf.get('mongo:poolSize'), 10) || 10, + socketOptions: { autoReconnect: true, keepAlive: nconf.get('mongo:keepAlive') || 0 }, + reconnectTries: 3600, + reconnectInterval: 1000, }, }; - connOptions = _.deepExtend((nconf.get('mongo:options') || {}), connOptions); + connOptions = _.deepExtend(connOptions, nconf.get('mongo:options') || {}); mongoClient.connect(connString, connOptions, function (err, _db) { if (err) { @@ -107,10 +105,7 @@ if (nconf.get('mongo:password') && nconf.get('mongo:username')) { db.authenticate(nconf.get('mongo:username'), nconf.get('mongo:password'), function (err) { - if (err) { - return callback(err); - } - callback(); + callback(err); }); } else { winston.warn('You have no mongo password setup!'); diff --git a/src/groups/update.js b/src/groups/update.js index 99e8dcc65a..9bf53886e5 100644 --- a/src/groups/update.js +++ b/src/groups/update.js @@ -91,16 +91,18 @@ module.exports = function (Groups) { async.apply(db.sortedSetRemove, 'groups:visible:name', groupName.toLowerCase() + ':' + groupName), ], callback); } else { - db.getObjectFields('group:' + groupName, ['createtime', 'memberCount'], function (err, groupData) { - if (err) { - return callback(err); - } - async.parallel([ - async.apply(db.sortedSetAdd, 'groups:visible:createtime', groupData.createtime, groupName), - async.apply(db.sortedSetAdd, 'groups:visible:memberCount', groupData.memberCount, groupName), - async.apply(db.sortedSetAdd, 'groups:visible:name', 0, groupName.toLowerCase() + ':' + groupName), - ], callback); - }); + async.waterfall([ + function (next) { + db.getObjectFields('group:' + groupName, ['createtime', 'memberCount'], next); + }, + function (groupData, next) { + async.parallel([ + async.apply(db.sortedSetAdd, 'groups:visible:createtime', groupData.createtime, groupName), + async.apply(db.sortedSetAdd, 'groups:visible:memberCount', groupData.memberCount, groupName), + async.apply(db.sortedSetAdd, 'groups:visible:name', 0, groupName.toLowerCase() + ':' + groupName), + ], next); + }, + ], callback); } } @@ -155,40 +157,48 @@ module.exports = function (Groups) { function checkNameChange(currentName, newName, callback) { if (currentName === newName) { - return callback(); + return setImmediate(callback); } var currentSlug = utils.slugify(currentName); var newSlug = utils.slugify(newName); if (currentSlug === newSlug) { - return callback(); + return setImmediate(callback); } - Groups.existsBySlug(newSlug, function (err, exists) { - if (err || exists) { - return callback(err || new Error('[[error:group-already-exists]]')); - } - callback(); - }); + async.waterfall([ + function (next) { + Groups.existsBySlug(newSlug, next); + }, + function (exists, next) { + next(exists ? new Error('[[error:group-already-exists]]') : null); + }, + ], callback); } function renameGroup(oldName, newName, callback) { if (oldName === newName || !newName || newName.length === 0) { - return callback(); + return setImmediate(callback); } + var group; + async.waterfall([ + function (next) { + db.getObject('group:' + oldName, next); + }, + function (_group, next) { + group = _group; + if (!group) { + return callback(); + } - db.getObject('group:' + oldName, function (err, group) { - if (err || !group) { - return callback(err); - } - - if (parseInt(group.system, 10) === 1) { - return callback(); - } - - Groups.exists(newName, function (err, exists) { - if (err || exists) { - return callback(err || new Error('[[error:group-already-exists]]')); + if (parseInt(group.system, 10) === 1) { + return callback(new Error('[[error:not-allowed-to-rename-system-group]]')); } + Groups.exists(newName, next); + }, + function (exists, next) { + if (exists) { + return callback(new Error('[[error:group-already-exists]]')); + } async.series([ async.apply(db.setObjectField, 'group:' + oldName, 'name', newName), async.apply(db.setObjectField, 'group:' + oldName, 'slug', utils.slugify(newName)), @@ -222,29 +232,33 @@ module.exports = function (Groups) { next(); }, - ], callback); - }); + ], next); + }, + ], function (err) { + callback(err); }); } function renameGroupMember(group, oldName, newName, callback) { - db.isSortedSetMember(group, oldName, function (err, isMember) { - if (err || !isMember) { - return callback(err); - } - var score; - async.waterfall([ - function (next) { - db.sortedSetScore(group, oldName, next); - }, - function (_score, next) { - score = _score; - db.sortedSetRemove(group, oldName, next); - }, - function (next) { - db.sortedSetAdd(group, score, newName, next); - }, - ], callback); - }); + var score; + async.waterfall([ + function (next) { + db.isSortedSetMember(group, oldName, next); + }, + function (isMember, next) { + if (!isMember) { + return callback(); + } + + db.sortedSetScore(group, oldName, next); + }, + function (_score, next) { + score = _score; + db.sortedSetRemove(group, oldName, next); + }, + function (next) { + db.sortedSetAdd(group, score, newName, next); + }, + ], callback); } }; diff --git a/src/middleware/render.js b/src/middleware/render.js index 48d4a0b526..88e9879887 100644 --- a/src/middleware/render.js +++ b/src/middleware/render.js @@ -123,7 +123,7 @@ module.exports = function (middleware) { winston.error(err.message); p = ''; } - + p = validator.escape(String(p)); parts[index] = index ? parts[0] + '-' + p : 'page-' + (p || 'home'); }); return parts.join(' '); diff --git a/src/posts/parse.js b/src/posts/parse.js index 946c589056..8ac2028bbd 100644 --- a/src/posts/parse.js +++ b/src/posts/parse.js @@ -1,5 +1,6 @@ 'use strict'; +var async = require('async'); var nconf = require('nconf'); var url = require('url'); var winston = require('winston'); @@ -14,31 +15,26 @@ var urlRegex = /href="([^"]+)"/g; module.exports = function (Posts) { Posts.parsePost = function (postData, callback) { - postData.content = postData.content || ''; + postData.content = String(postData.content || ''); if (postData.pid && cache.has(String(postData.pid))) { postData.content = cache.get(String(postData.pid)); return callback(null, postData); } - // Casting post content into a string, just in case - if (typeof postData.content !== 'string') { - postData.content = postData.content.toString(); - } - - plugins.fireHook('filter:parse.post', { postData: postData }, function (err, data) { - if (err) { - return callback(err); - } - - data.postData.content = translator.escape(data.postData.content); + async.waterfall([ + function (next) { + plugins.fireHook('filter:parse.post', { postData: postData }, next); + }, + function (data, next) { + data.postData.content = translator.escape(data.postData.content); - if (global.env === 'production' && data.postData.pid) { - cache.set(String(data.postData.pid), data.postData.content); - } - - callback(null, data.postData); - }); + if (global.env === 'production' && data.postData.pid) { + cache.set(String(data.postData.pid), data.postData.content); + } + next(null, data.postData); + }, + ], callback); }; Posts.parseSignature = function (userData, uid, callback) { @@ -51,7 +47,6 @@ module.exports = function (Posts) { var parsed; var current = urlRegex.exec(content); var absolute; - while (current !== null) { if (current[1]) { try { @@ -78,7 +73,7 @@ module.exports = function (Posts) { }; function sanitizeSignature(signature) { - var string = S(signature); + var string = S(signature); var tagsToStrip = []; if (parseInt(meta.config['signatures:disableLinks'], 10) === 1) { diff --git a/src/topics.js b/src/topics.js index 77cea4ec98..164200016d 100644 --- a/src/topics.js +++ b/src/topics.js @@ -180,6 +180,7 @@ var social = require('./social'); isIgnoring: async.apply(Topics.isIgnoring, [topicData.tid], uid), bookmark: async.apply(Topics.getUserBookmark, topicData.tid, uid), postSharing: async.apply(social.getActivePostSharing), + deleter: async.apply(getDeleter, topicData), related: function (next) { async.waterfall([ function (next) { @@ -202,6 +203,8 @@ var social = require('./social'); topicData.isIgnoring = results.isIgnoring[0]; topicData.bookmark = results.bookmark; topicData.postSharing = results.postSharing; + topicData.deleter = results.deleter; + topicData.deletedTimestampISO = utils.toISOString(topicData.deletedTimestamp); topicData.related = results.related || []; topicData.unreplied = parseInt(topicData.postcount, 10) === 1; @@ -258,6 +261,13 @@ var social = require('./social'); ], callback); } + function getDeleter(topicData, callback) { + if (!topicData.deleterUid) { + return setImmediate(callback, null, null); + } + user.getUserFields(topicData.deleterUid, ['username', 'userslug', 'picture'], callback); + } + Topics.getMainPost = function (tid, uid, callback) { Topics.getMainPosts([tid], uid, function (err, mainPosts) { callback(err, Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null); diff --git a/src/topics/data.js b/src/topics/data.js index cf1df2787c..17e060f679 100644 --- a/src/topics/data.js +++ b/src/topics/data.js @@ -86,4 +86,8 @@ module.exports = function (Topics) { Topics.deleteTopicField = function (tid, field, callback) { db.deleteObjectField('topic:' + tid, field, callback); }; + + Topics.deleteTopicFields = function (tid, fields, callback) { + db.deleteObjectFields('topic:' + tid, fields, callback); + }; }; diff --git a/src/topics/delete.js b/src/topics/delete.js index ec9d7f3a42..3598248d26 100644 --- a/src/topics/delete.js +++ b/src/topics/delete.js @@ -18,7 +18,11 @@ module.exports = function (Topics) { async.parallel([ function (next) { - Topics.setTopicField(tid, 'deleted', 1, next); + Topics.setTopicFields(tid, { + deleted: 1, + deleterUid: uid, + deletedTimestamp: Date.now(), + }, next); }, function (next) { db.sortedSetsRemove(['topics:recent', 'topics:posts', 'topics:views'], tid, next); @@ -47,6 +51,9 @@ module.exports = function (Topics) { function (next) { Topics.setTopicField(tid, 'deleted', 0, next); }, + function (next) { + Topics.deleteTopicFields(tid, ['deleterUid', 'deletedTimestamp'], next); + }, function (next) { Topics.updateRecent(tid, topicData.lastposttime, next); }, diff --git a/src/topics/unread.js b/src/topics/unread.js index 3e8fb55da9..50c65e2511 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -204,7 +204,7 @@ module.exports = function (Topics) { Topics.markAsRead = function (tids, uid, callback) { callback = callback || function () {}; if (!Array.isArray(tids) || !tids.length) { - return callback(); + return setImmediate(callback, null, false); } tids = tids.filter(function (tid, index, array) { @@ -212,7 +212,7 @@ module.exports = function (Topics) { }); if (!tids.length) { - return callback(null, false); + return setImmediate(callback, null, false); } async.waterfall([ diff --git a/src/views/admin/settings/general.tpl b/src/views/admin/settings/general.tpl index 68705951da..2d9a198ec8 100644 --- a/src/views/admin/settings/general.tpl +++ b/src/views/admin/settings/general.tpl @@ -31,8 +31,8 @@
- -
+
+
@@ -140,6 +140,11 @@ [[admin/settings/general:outgoing-links.warning-page]] + +
+
+ +
diff --git a/src/views/admin/settings/group.tpl b/src/views/admin/settings/group.tpl index 1c0b660361..fd696cb5ad 100644 --- a/src/views/admin/settings/group.tpl +++ b/src/views/admin/settings/group.tpl @@ -43,7 +43,7 @@

[[admin/settings/group:default-cover-help]]

-
+
diff --git a/src/views/admin/settings/uploads.tpl b/src/views/admin/settings/uploads.tpl index 6c9b59db80..feff2dbff3 100644 --- a/src/views/admin/settings/uploads.tpl +++ b/src/views/admin/settings/uploads.tpl @@ -50,7 +50,7 @@
- +

[[admin/settings/uploads:allowed-file-extensions-help]]

@@ -131,7 +131,7 @@

[[admin/settings/uploads:default-covers-help]]

- +
diff --git a/test/groups.js b/test/groups.js index 4d587b3aad..6b52787efa 100644 --- a/test/groups.js +++ b/test/groups.js @@ -315,6 +315,15 @@ describe('Groups', function () { }); }); }); + + it('should fail if system groups is being renamed', function (done) { + Groups.update('administrators', { + name: 'administrators_fail', + }, function (err) { + assert.equal(err.message, '[[error:not-allowed-to-rename-system-group]]'); + done(); + }); + }); }); describe('.destroy()', function () { diff --git a/test/posts.js b/test/posts.js index 9800410a29..8508ab70f4 100644 --- a/test/posts.js +++ b/test/posts.js @@ -534,6 +534,50 @@ describe('Post\'s', function () { }); }); + describe('parse', function () { + it('should store post content in cache', function (done) { + var oldValue = global.env; + global.env = 'production'; + var postData = { + pid: 9999, + content: 'some post content', + }; + posts.parsePost(postData, function (err) { + assert.ifError(err); + posts.parsePost(postData, function (err) { + assert.ifError(err); + global.env = oldValue; + done(); + }); + }); + }); + + it('should parse signature and remove links and images', function (done) { + var meta = require('../src/meta'); + meta.config['signatures:disableLinks'] = 1; + meta.config['signatures:disableImages'] = 1; + var userData = { + signature: 'test derp', + }; + + posts.parseSignature(userData, 1, function (err, data) { + assert.ifError(err); + assert.equal(data.userData.signature, 'test derp'); + meta.config['signatures:disableLinks'] = 0; + meta.config['signatures:disableImages'] = 0; + done(); + }); + }); + + it('should turn relative links in post body to absolute urls', function (done) { + var nconf = require('nconf'); + var content = 'test youtube'; + var parsedContent = posts.relativeToAbsolute(content); + assert.equal(parsedContent, 'test youtube'); + done(); + }); + }); + describe('socket methods', function () { var pid; before(function (done) { @@ -600,7 +644,7 @@ describe('Post\'s', function () { }); it('shold error with invalid data', function (done) { - socketPosts.loadMoreBookmarks({ uid: voterUid }, { uid: voterUid, after: null }, function (err, postData) { + socketPosts.loadMoreBookmarks({ uid: voterUid }, { uid: voterUid, after: null }, function (err) { assert.equal(err.message, '[[error:invalid-data]]'); done(); }); diff --git a/test/topics.js b/test/topics.js index ad481ade1e..6782f1041d 100644 --- a/test/topics.js +++ b/test/topics.js @@ -1181,6 +1181,14 @@ describe('Topic\'s', function () { }); }); }); + + it('should not do anything if tids is empty array', function (done) { + socketTopics.markAsRead({ uid: adminUid }, [], function (err, markedRead) { + assert.ifError(err); + assert(!markedRead); + done(); + }); + }); }); describe('tags', function () {