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]]
+
+
[[admin/settings/group:default-cover-help]]
-[[admin/settings/uploads:allowed-file-extensions-help]]
@@ -131,7 +131,7 @@[[admin/settings/uploads:default-covers-help]]
- +