mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-03-09 22:20:48 +01:00
Merge remote-tracking branch 'refs/remotes/origin/master' into develop
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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": "已接收的连接总数",
|
||||
|
||||
@@ -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": "计数",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"events": "事件",
|
||||
"no-events": "暂无事件。",
|
||||
"no-events": "暂无事件",
|
||||
"control-panel": "事件控制面板",
|
||||
"delete-events": "清除事件"
|
||||
}
|
||||
@@ -5,5 +5,5 @@
|
||||
"current-skin": "当前皮肤",
|
||||
"skin-updated": "皮肤已更新",
|
||||
"applied-success": "%1 皮肤已成功应用",
|
||||
"revert-success": "皮肤恢复到基础颜色"
|
||||
"revert-success": "皮肤已恢复到基础颜色"
|
||||
}
|
||||
@@ -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": "更新日志记录器设置"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"keep-updated": "请确保您已及时更新 NodeBB 以获得最新的安全补丁与 Bug 修复。",
|
||||
"up-to-date": "<p>正在使用 <strong>最新版本</strong> <i class=\"fa fa-check\"></i></p>",
|
||||
"upgrade-available": "<p> 新版本 (v%1) 已经发布! 建议 <a href=\"https://docs.nodebb.org/configuring/upgrade/\">更新你的 NodeBB</a>。</p>",
|
||||
"prerelease-upgrade-available": "<p>正在使用NodeBB过期的实验版本。新的版本 (v%1) 已经发布。 请考虑<a href=\"https://docs.nodebb.org/configuring/upgrade/\">更新你的 NodeBB</a>。",
|
||||
"prerelease-upgrade-available": "<p>正在使用过时的测试版 NodeBB。新的版本 (v%1) 已经发布。 请考虑<a href=\"https://docs.nodebb.org/configuring/upgrade/\">更新你的 NodeBB</a>。",
|
||||
"prerelease-warning": "<p>正在使用<strong>测试版</strong> NodeBB。可能会出现意外的 Bug。<i class=\"fa fa-exclamation-triangle\"></i></p>",
|
||||
"running-in-development": "<span>论坛正处于开发模式,这可能使其暴露于潜在的危险之中;请联系您的系统管理员。</span>",
|
||||
|
||||
|
||||
@@ -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": "在这里查找用户…",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"list.ip-spam": "频率:%1 显示: %2",
|
||||
|
||||
"invitations": "邀请",
|
||||
"invitations.description": "下面是一份完整的邀请请求列表。请使用Ctrl-F键以及电子邮件或者用户名以便搜索这个列表。<br><br>那些已经接受他们邀请的用户的用户名将显示在电子邮箱右边。",
|
||||
"invitations.description": "下面列出了所有已发送的邀请。您可以使用 Ctrl+F 快捷键搜索列表中的邮箱地址或用户名。<br><br>如果用户接受了邀请,他的用户名将会被显示在邮箱右边。",
|
||||
"invitations.inviter-username": "邀请人用户名",
|
||||
"invitations.invitee-email": "受邀请的电子邮箱",
|
||||
"invitations.invitee-username": "受邀请的用户名(如果已经注册)",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"category": "版块",
|
||||
"subcategories": "子版块",
|
||||
"new_topic_button": "新主题",
|
||||
"guest-login-post": "登录后发表",
|
||||
"guest-login-post": "登录以发表",
|
||||
"no_topics": "<strong>此版块还没有任何内容。</strong><br />赶紧来发帖吧!",
|
||||
"browsing": "正在浏览",
|
||||
"no_replies": "尚无回复",
|
||||
@@ -10,7 +10,7 @@
|
||||
"share_this_category": "分享此版块",
|
||||
"watch": "关注",
|
||||
"ignore": "忽略",
|
||||
"watching": "正在关注",
|
||||
"watching": "已关注",
|
||||
"ignoring": "已忽略",
|
||||
"watching.description": "显示未读主题",
|
||||
"ignoring.description": "不显示未读主题",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -70,7 +70,6 @@ Notifications.getMultiple = function (nids, callback) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
next(null, notifications);
|
||||
},
|
||||
], callback);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: '<svg/onload=alert(document.location);//' }, next);
|
||||
},
|
||||
function (next) {
|
||||
User.getModerationNotes(testUid, 0, -1, next);
|
||||
},
|
||||
], function (err, notes) {
|
||||
assert.ifError(err);
|
||||
assert.equal(notes[0].note, 'second moderation note');
|
||||
assert.equal(notes[0].note, '<svg/onload=alert(document.location);//');
|
||||
assert.equal(notes[0].uid, adminUid);
|
||||
assert.equal(notes[1].note, 'this is a test user');
|
||||
assert(notes[0].timestamp);
|
||||
done();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user