diff --git a/package.json b/package.json
index c12be8d8db..b2b7e352c0 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"express-useragent": "1.0.7",
"html-to-text": "3.3.0",
"ip": "1.1.5",
+ "ip-range-check": "^0.0.2",
"jimp": "0.2.28",
"jquery": "^3.1.0",
"json-2-csv": "^2.0.22",
@@ -55,7 +56,7 @@
"morgan": "^1.3.2",
"mousetrap": "^1.5.3",
"nconf": "~0.8.2",
- "nodebb-plugin-composer-default": "5.0.5",
+ "nodebb-plugin-composer-default": "5.0.6",
"nodebb-plugin-dbsearch": "2.0.6",
"nodebb-plugin-emoji-extended": "1.1.1",
"nodebb-plugin-emoji-one": "1.2.1",
@@ -65,9 +66,9 @@
"nodebb-plugin-spam-be-gone": "0.5.1",
"nodebb-rewards-essentials": "0.0.9",
"nodebb-theme-lavender": "4.0.5",
- "nodebb-theme-persona": "5.0.22",
+ "nodebb-theme-persona": "5.0.30",
"nodebb-theme-slick": "1.1.0",
- "nodebb-theme-vanilla": "6.0.17",
+ "nodebb-theme-vanilla": "6.0.24",
"nodebb-widget-essentials": "3.0.1",
"nodemailer": "2.6.4",
"nodemailer-sendmail-transport": "1.0.0",
diff --git a/public/language/zh-CN/admin/advanced/database.json b/public/language/zh-CN/admin/advanced/database.json
index 825469bb2d..f8487c7fcb 100644
--- a/public/language/zh-CN/admin/advanced/database.json
+++ b/public/language/zh-CN/admin/advanced/database.json
@@ -17,14 +17,14 @@
"mongo.file-size": "文件大小",
"mongo.resident-memory": "驻留内存",
"mongo.virtual-memory": "虚拟内存",
- "mongo.mapped-memory": "映射",
+ "mongo.mapped-memory": "已映射内存",
"mongo.raw-info": "MongoDB 原始信息",
"redis": "Redis",
"redis.version": "Redis 版本",
"redis.connected-clients": "已连接客户端",
"redis.connected-slaves": "已连接从",
- "redis.blocked-clients": "阻止的客户端",
+ "redis.blocked-clients": "受阻的客户端",
"redis.used-memory": "已使用内存",
"redis.memory-frag-ratio": "内存碎片比率",
"redis.total-connections-recieved": "已接收的连接总数",
diff --git a/public/language/zh-CN/admin/advanced/errors.json b/public/language/zh-CN/admin/advanced/errors.json
index 257f88661c..6fd8b532b6 100644
--- a/public/language/zh-CN/admin/advanced/errors.json
+++ b/public/language/zh-CN/admin/advanced/errors.json
@@ -4,7 +4,7 @@
"error.404": "404 页面不存在",
"error.503": "503 服务不可用",
"manage-error-log": "管理错误日志",
- "export-error-log": "提取错误日志(.csv)",
+ "export-error-log": "导出错误日志 (.csv)",
"clear-error-log": "清空错误日志",
"route": "路由",
"count": "计数",
diff --git a/public/language/zh-CN/admin/advanced/events.json b/public/language/zh-CN/admin/advanced/events.json
index 85d741ff27..b9f44389e7 100644
--- a/public/language/zh-CN/admin/advanced/events.json
+++ b/public/language/zh-CN/admin/advanced/events.json
@@ -1,6 +1,6 @@
{
"events": "事件",
- "no-events": "暂无事件。",
+ "no-events": "暂无事件",
"control-panel": "事件控制面板",
"delete-events": "清除事件"
}
\ No newline at end of file
diff --git a/public/language/zh-CN/admin/appearance/skins.json b/public/language/zh-CN/admin/appearance/skins.json
index 730c79c2f0..1a075fe9f9 100644
--- a/public/language/zh-CN/admin/appearance/skins.json
+++ b/public/language/zh-CN/admin/appearance/skins.json
@@ -5,5 +5,5 @@
"current-skin": "当前皮肤",
"skin-updated": "皮肤已更新",
"applied-success": "%1 皮肤已成功应用",
- "revert-success": "皮肤恢复到基础颜色"
+ "revert-success": "皮肤已恢复到基础颜色"
}
\ No newline at end of file
diff --git a/public/language/zh-CN/admin/development/logger.json b/public/language/zh-CN/admin/development/logger.json
index b7e6b36f17..769fd15b11 100644
--- a/public/language/zh-CN/admin/development/logger.json
+++ b/public/language/zh-CN/admin/development/logger.json
@@ -5,7 +5,7 @@
"enable-http": "启用 HTTP 日志",
"enable-socket": "启用 socket.io 事件日志",
"file-path": "日志文件路径",
- "file-path-placeholder": "如/path/to/log/file.log ::: 如想在终端中显示日志请留空此项",
+ "file-path-placeholder": "如 /path/to/log/file.log ::: 如想在终端中显示日志请留空此项",
"control-panel": "日志记录器控制面板",
"update-settings": "更新日志记录器设置"
diff --git a/public/language/zh-CN/admin/general/dashboard.json b/public/language/zh-CN/admin/general/dashboard.json
index f89eecfc59..f263627aa2 100644
--- a/public/language/zh-CN/admin/general/dashboard.json
+++ b/public/language/zh-CN/admin/general/dashboard.json
@@ -24,7 +24,7 @@
"keep-updated": "请确保您已及时更新 NodeBB 以获得最新的安全补丁与 Bug 修复。",
"up-to-date": "
正在使用 最新版本
",
"upgrade-available": " 新版本 (v%1) 已经发布! 建议 更新你的 NodeBB。
",
- "prerelease-upgrade-available": "正在使用NodeBB过期的实验版本。新的版本 (v%1) 已经发布。 请考虑更新你的 NodeBB。",
+ "prerelease-upgrade-available": "
正在使用过时的测试版 NodeBB。新的版本 (v%1) 已经发布。 请考虑更新你的 NodeBB。",
"prerelease-warning": "
正在使用测试版 NodeBB。可能会出现意外的 Bug。
",
"running-in-development": "论坛正处于开发模式,这可能使其暴露于潜在的危险之中;请联系您的系统管理员。",
diff --git a/public/language/zh-CN/admin/manage/categories.json b/public/language/zh-CN/admin/manage/categories.json
index babc97bb9f..63f94bdebe 100644
--- a/public/language/zh-CN/admin/manage/categories.json
+++ b/public/language/zh-CN/admin/manage/categories.json
@@ -60,7 +60,7 @@
"alert.copy-success": "设置已复制!",
"alert.set-parent-category": "设置父版块",
"alert.updated": "版块已更新",
- "alert.updated-success": "版块ID %1 成功更新。",
+ "alert.updated-success": "版块 ID %1 成功更新。",
"alert.upload-image": "上传版块图片",
"alert.find-user": "查找用户",
"alert.user-search": "在这里查找用户…",
diff --git a/public/language/zh-CN/admin/manage/registration.json b/public/language/zh-CN/admin/manage/registration.json
index aa46fcdb49..fa2e26c5be 100644
--- a/public/language/zh-CN/admin/manage/registration.json
+++ b/public/language/zh-CN/admin/manage/registration.json
@@ -11,7 +11,7 @@
"list.ip-spam": "频率:%1 显示: %2",
"invitations": "邀请",
- "invitations.description": "下面是一份完整的邀请请求列表。请使用Ctrl-F键以及电子邮件或者用户名以便搜索这个列表。
那些已经接受他们邀请的用户的用户名将显示在电子邮箱右边。",
+ "invitations.description": "下面列出了所有已发送的邀请。您可以使用 Ctrl+F 快捷键搜索列表中的邮箱地址或用户名。
如果用户接受了邀请,他的用户名将会被显示在邮箱右边。",
"invitations.inviter-username": "邀请人用户名",
"invitations.invitee-email": "受邀请的电子邮箱",
"invitations.invitee-username": "受邀请的用户名(如果已经注册)",
diff --git a/public/language/zh-CN/category.json b/public/language/zh-CN/category.json
index e9ce41a2e3..4c9902d3d8 100644
--- a/public/language/zh-CN/category.json
+++ b/public/language/zh-CN/category.json
@@ -2,7 +2,7 @@
"category": "版块",
"subcategories": "子版块",
"new_topic_button": "新主题",
- "guest-login-post": "登录后发表",
+ "guest-login-post": "登录以发表",
"no_topics": "此版块还没有任何内容。
赶紧来发帖吧!",
"browsing": "正在浏览",
"no_replies": "尚无回复",
@@ -10,7 +10,7 @@
"share_this_category": "分享此版块",
"watch": "关注",
"ignore": "忽略",
- "watching": "正在关注",
+ "watching": "已关注",
"ignoring": "已忽略",
"watching.description": "显示未读主题",
"ignoring.description": "不显示未读主题",
diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js
index 928884b69a..f99e1b8de4 100644
--- a/public/src/modules/notifications.js
+++ b/public/src/modules/notifications.js
@@ -1,7 +1,7 @@
'use strict';
-define('notifications', ['sounds', 'translator', 'components'], function (sounds, translator, components) {
+define('notifications', ['sounds', 'translator', 'components', 'navigator'], function (sounds, translator, components, navigator) {
var Notifications = {};
var unreadNotifs = {};
@@ -20,12 +20,19 @@ define('notifications', ['sounds', 'translator', 'components'], function (sounds
Notifications.loadNotifications(notifList);
});
- notifList.on('click', '[data-nid]', function () {
- var unread = $(this).hasClass('unread');
+ notifList.on('click', '[data-nid]', function (ev) {
+ var notifEl = $(this);
+ if (scrollToPostIndexIfOnPage(notifEl)) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ notifTrigger.dropdown('toggle');
+ }
+
+ var unread = notifEl.hasClass('unread');
if (!unread) {
return;
}
- var nid = $(this).attr('data-nid');
+ var nid = notifEl.attr('data-nid');
socket.emit('notifications.markRead', nid, function (err) {
if (err) {
return app.alertError(err.message);
@@ -107,6 +114,19 @@ define('notifications', ['sounds', 'translator', 'components'], function (sounds
});
};
+ function scrollToPostIndexIfOnPage(notifEl) {
+ // Scroll to index if already in topic (gh#5873)
+ var pid = notifEl.attr('data-pid');
+ var tid = notifEl.attr('data-tid');
+ var path = notifEl.attr('data-path');
+ var postEl = components.get('post', 'pid', pid);
+ if (path.startsWith(config.relative_path + '/post/') && pid && postEl.length && ajaxify.data.template.topic && parseInt(ajaxify.data.tid, 10) === parseInt(tid, 10)) {
+ navigator.scrollToIndex(postEl.attr('data-index'), true);
+ return true;
+ }
+ return false;
+ }
+
Notifications.loadNotifications = function (notifList) {
socket.emit('notifications.get', null, function (err, data) {
if (err) {
diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js
index 5bd65a33af..e35ac514d9 100644
--- a/src/controllers/accounts/helpers.js
+++ b/src/controllers/accounts/helpers.js
@@ -182,9 +182,8 @@ function filterLinks(links, states) {
admin: true,
}, link.visibility);
- // Iterate through states and permit if every test passes (or is not defined)
var permit = Object.keys(states).some(function (state) {
- return states[state] === link.visibility[state];
+ return states[state] && link.visibility[state];
});
links[index].public = permit;
diff --git a/src/controllers/accounts/info.js b/src/controllers/accounts/info.js
index 7f9edb2462..cb3b6f1abf 100644
--- a/src/controllers/accounts/info.js
+++ b/src/controllers/accounts/info.js
@@ -47,24 +47,21 @@ infoController.get = function (req, res, callback) {
},
}, next);
},
- ], function (err, data) {
- if (err) {
- return callback(err);
- }
+ function (data) {
+ userData.history = data.history;
+ userData.sessions = data.sessions;
+ userData.usernames = data.usernames;
+ userData.emails = data.emails;
- userData.history = data.history;
- userData.sessions = data.sessions;
- userData.usernames = data.usernames;
- userData.emails = data.emails;
-
- if (userData.isAdminOrGlobalModeratorOrModerator) {
- userData.moderationNotes = data.notes.notes;
- var pageCount = Math.ceil(data.notes.count / itemsPerPage);
- userData.pagination = pagination.create(page, pageCount, req.query);
- }
- userData.title = '[[pages:account/info]]';
- userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[user:account_info]]' }]);
+ if (userData.isAdminOrGlobalModeratorOrModerator) {
+ userData.moderationNotes = data.notes.notes;
+ var pageCount = Math.ceil(data.notes.count / itemsPerPage);
+ userData.pagination = pagination.create(page, pageCount, req.query);
+ }
+ userData.title = '[[pages:account/info]]';
+ userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[user:account_info]]' }]);
- res.render('account/info', userData);
- });
+ res.render('account/info', userData);
+ },
+ ], callback);
};
diff --git a/src/meta/blacklist.js b/src/meta/blacklist.js
index 1ef6cfe0a9..80f3ca1190 100644
--- a/src/meta/blacklist.js
+++ b/src/meta/blacklist.js
@@ -1,6 +1,7 @@
'use strict';
var ip = require('ip');
+var ipRangeCheck = require('ip-range-check');
var winston = require('winston');
var async = require('async');
@@ -27,6 +28,7 @@ Blacklist.load = function (callback) {
ipv4: rules.ipv4,
ipv6: rules.ipv6,
cidr: rules.cidr,
+ cidr6: rules.cidr6,
};
next();
},
@@ -53,11 +55,12 @@ Blacklist.get = function (callback) {
Blacklist.test = function (clientIp, callback) {
if (
- Blacklist._rules.ipv4.indexOf(clientIp) === -1 &&// not explicitly specified in ipv4 list
- Blacklist._rules.ipv6.indexOf(clientIp) === -1 &&// not explicitly specified in ipv6 list
+ Blacklist._rules.ipv4.indexOf(clientIp) === -1 && // not explicitly specified in ipv4 list
+ Blacklist._rules.ipv6.indexOf(clientIp) === -1 && // not explicitly specified in ipv6 list
!Blacklist._rules.cidr.some(function (subnet) {
return ip.cidrSubnet(subnet).contains(clientIp);
- }) // not in a blacklisted cidr range
+ }) && // not in a blacklisted IPv4 cidr range
+ !ipRangeCheck(clientIp, Blacklist._rules.cidr6) // not in a blacklisted IPv6 cidr range
) {
if (typeof callback === 'function') {
setImmediate(callback);
@@ -81,9 +84,11 @@ Blacklist.validate = function (rules, callback) {
var ipv4 = [];
var ipv6 = [];
var cidr = [];
+ var cidr6 = [];
var invalid = [];
- var isCidrSubnet = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/;
+ var isIPv4CidrSubnet = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/;
+ var isIPv6CidrSubnet = /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/;
var inlineCommentMatch = /#.*$/;
var whitelist = ['127.0.0.1', '::1', '::ffff:0:127.0.0.1'];
@@ -109,7 +114,11 @@ Blacklist.validate = function (rules, callback) {
ipv6.push(rule);
return true;
}
- if (isCidrSubnet.test(rule)) {
+ if (isIPv4CidrSubnet.test(rule)) {
+ cidr.push(rule);
+ return true;
+ }
+ if (isIPv6CidrSubnet.test(rule)) {
cidr.push(rule);
return true;
}
@@ -123,6 +132,7 @@ Blacklist.validate = function (rules, callback) {
ipv4: ipv4,
ipv6: ipv6,
cidr: cidr,
+ cidr6: cidr6,
valid: rules,
invalid: invalid,
});
diff --git a/src/notifications.js b/src/notifications.js
index 58ccf2e8fa..098efe5d9f 100644
--- a/src/notifications.js
+++ b/src/notifications.js
@@ -70,7 +70,6 @@ Notifications.getMultiple = function (nids, callback) {
}
}
});
-
next(null, notifications);
},
], callback);
diff --git a/src/privileges/posts.js b/src/privileges/posts.js
index 89ef1e0f48..f2bfe38428 100644
--- a/src/privileges/posts.js
+++ b/src/privileges/posts.js
@@ -10,6 +10,7 @@ var topics = require('../topics');
var user = require('../user');
var helpers = require('./helpers');
var plugins = require('../plugins');
+var utils = require('../utils');
module.exports = function (privileges) {
privileges.posts = {};
@@ -190,6 +191,22 @@ module.exports = function (privileges) {
], callback);
};
+ privileges.posts.canFlag = function (pid, uid, callback) {
+ async.waterfall([
+ function (next) {
+ async.parallel({
+ userReputation: async.apply(user.getUserField, uid, 'reputation'),
+ isAdminOrMod: async.apply(isAdminOrMod, pid, uid),
+ }, next);
+ },
+ function (results, next) {
+ var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
+ var canFlag = results.isAdminOrMod || parseInt(results.userReputation, 10) >= minimumReputation;
+ next(null, { flag: canFlag });
+ },
+ ], callback);
+ };
+
privileges.posts.canMove = function (pid, uid, callback) {
async.waterfall([
function (next) {
diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js
index c075a96a8e..7f80ce9805 100644
--- a/src/socket.io/posts/tools.js
+++ b/src/socket.io/posts/tools.js
@@ -31,6 +31,9 @@ module.exports = function (SocketPosts) {
canDelete: function (next) {
privileges.posts.canDelete(data.pid, socket.uid, next);
},
+ canFlag: function (next) {
+ privileges.posts.canFlag(data.pid, socket.uid, next);
+ },
bookmarked: function (next) {
posts.hasBookmarked(data.pid, socket.uid, next);
},
@@ -49,6 +52,7 @@ module.exports = function (SocketPosts) {
results.posts.selfPost = socket.uid && socket.uid === parseInt(results.posts.uid, 10);
results.posts.display_edit_tools = results.canEdit.flag;
results.posts.display_delete_tools = results.canDelete.flag;
+ results.posts.display_flag_tools = socket.uid && !results.posts.selfPost && results.canFlag.flag;
results.posts.display_moderator_tools = results.posts.display_edit_tools || results.posts.display_delete_tools;
results.posts.display_move_tools = results.isAdminOrMod;
next(null, results);
diff --git a/src/user/info.js b/src/user/info.js
index e8642989a1..5e91c6cf08 100644
--- a/src/user/info.js
+++ b/src/user/info.js
@@ -166,6 +166,7 @@ module.exports = function (User) {
var data = JSON.parse(note);
uids.push(data.uid);
data.timestampISO = utils.toISOString(data.timestamp);
+ data.note = validator.escape(String(data.note));
return data;
} catch (err) {
return next(err);
diff --git a/src/user/profile.js b/src/user/profile.js
index 54ed58267b..dbdcbcba85 100644
--- a/src/user/profile.js
+++ b/src/user/profile.js
@@ -185,7 +185,9 @@ module.exports = function (User) {
function (next) {
db.sortedSetAdd('users:notvalidated', Date.now(), uid, next);
},
- async.apply(User.reset.cleanByUid, uid),
+ function (next) {
+ User.reset.cleanByUid(uid, next);
+ },
], function (err) {
next(err);
});
diff --git a/src/user/reset.js b/src/user/reset.js
index e2982ca4d1..fd0b5daa53 100644
--- a/src/user/reset.js
+++ b/src/user/reset.js
@@ -170,17 +170,12 @@ UserReset.clean = function (callback) {
};
UserReset.cleanByUid = function (uid, callback) {
- if (typeof callback !== 'function') {
- callback = function () {};
- }
-
var toClean = [];
uid = parseInt(uid, 10);
async.waterfall([
- async.apply(db.getSortedSetRange.bind(db), 'reset:issueDate', 0, -1),
- function (tokens, next) {
- batch.processArray(tokens, function (tokens, next) {
+ function (next) {
+ batch.processSortedSet('reset:issueDate', function (tokens, next) {
db.getObjectFields('reset:uid', tokens, function (err, results) {
for (var code in results) {
if (results.hasOwnProperty(code) && parseInt(results[code], 10) === uid) {
diff --git a/test/user.js b/test/user.js
index 5a3f29aba8..733179be62 100644
--- a/test/user.js
+++ b/test/user.js
@@ -1236,15 +1236,16 @@ describe('User', function () {
setTimeout(next, 50);
},
function (next) {
- socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: 'second moderation note' }, next);
+ socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: '