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: '