diff --git a/install/package.json b/install/package.json
index 0b92fa2ea9..80dbcd4363 100644
--- a/install/package.json
+++ b/install/package.json
@@ -65,7 +65,7 @@
"mousetrap": "^1.6.1",
"mubsub-nbb": "^1.5.0",
"nconf": "^0.10.0",
- "nodebb-plugin-composer-default": "6.0.28",
+ "nodebb-plugin-composer-default": "6.0.29",
"nodebb-plugin-dbsearch": "2.0.19",
"nodebb-plugin-emoji": "^2.2.2",
"nodebb-plugin-emoji-android": "2.0.0",
@@ -75,9 +75,9 @@
"nodebb-plugin-spam-be-gone": "0.5.4",
"nodebb-rewards-essentials": "0.0.11",
"nodebb-theme-lavender": "5.0.5",
- "nodebb-theme-persona": "9.0.17",
- "nodebb-theme-slick": "1.2.5",
- "nodebb-theme-vanilla": "10.0.15",
+ "nodebb-theme-persona": "9.0.19",
+ "nodebb-theme-slick": "1.2.6",
+ "nodebb-theme-vanilla": "10.0.17",
"nodebb-widget-essentials": "4.0.7",
"nodemailer": "^4.6.5",
"passport": "^0.4.0",
diff --git a/public/language/bg/admin/manage/privileges.json b/public/language/bg/admin/manage/privileges.json
index 1128fca181..3c1d24bba8 100644
--- a/public/language/bg/admin/manage/privileges.json
+++ b/public/language/bg/admin/manage/privileges.json
@@ -20,7 +20,7 @@
"edit-posts": "Редактиране на публикации",
"view-edit-history": "Преглед на историята на редакциите",
"delete-posts": "Изтриване на публикации",
- "view_deleted": "View Deleted Posts",
+ "view_deleted": "Преглед на изтритите публикации",
"upvote-posts": "Положително гласуване за публикации",
"downvote-posts": "Отрицателно гласуване за публикации",
"delete-topics": "Изтриване на теми",
diff --git a/public/language/bg/admin/settings/advanced.json b/public/language/bg/admin/settings/advanced.json
index 7ae142a4e4..8e92299247 100644
--- a/public/language/bg/admin/settings/advanced.json
+++ b/public/language/bg/admin/settings/advanced.json
@@ -6,9 +6,9 @@
"headers.allow-from": "Задайте „ALLOW-FROM“, за да поставите NodeBB в „iFrame“",
"headers.powered-by": "Персонализиране на заглавната част „Захранван от“, която се изпраща от NodeBB",
"headers.acao": "Произход за разрешаване на управлението на достъпа",
- "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression",
+ "headers.acao-regex": "Регулярен израз за произхода за разрешаване на управлението на достъпа",
"headers.acao-help": "За да забраните достъпа до всички уеб сайтове, оставете празно",
- "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty",
+ "headers.acao-regex-help": "Въведете регулярен израз за съвпадение с динамичните произходи. За да забраните достъпа на всички уеб сайтове, оставете това празно.",
"headers.acac": "Удостоверителни данни за разрешаване на управлението на достъпа",
"headers.acam": "Методи за разрешаване на управлението на достъпа",
"headers.acah": "Заглавки за разрешаване на управлението на достъпа",
diff --git a/public/language/cs/admin/manage/privileges.json b/public/language/cs/admin/manage/privileges.json
index 2ca282d43e..0320ddcb94 100644
--- a/public/language/cs/admin/manage/privileges.json
+++ b/public/language/cs/admin/manage/privileges.json
@@ -20,7 +20,7 @@
"edit-posts": "Upravit příspěvek",
"view-edit-history": "Zobrazit historii editace",
"delete-posts": "Odstranit příspěvky",
- "view_deleted": "View Deleted Posts",
+ "view_deleted": "Zobrazit odstraněné příspěvky",
"upvote-posts": "Souhlasné příspěvky",
"downvote-posts": "Nesouhlasné příspěvky",
"delete-topics": "Odstranit témata",
diff --git a/public/language/cs/admin/settings/advanced.json b/public/language/cs/admin/settings/advanced.json
index 268c7429cc..5cf39f453d 100644
--- a/public/language/cs/admin/settings/advanced.json
+++ b/public/language/cs/admin/settings/advanced.json
@@ -8,7 +8,7 @@
"headers.acao": "Access-Control-Allow-Origin",
"headers.acao-regex": "Access-Control-Allow-Origin Regular Expression",
"headers.acao-help": "Pro zakázání přístupu na všechny stránky, zanechte prázdné",
- "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty",
+ "headers.acao-regex-help": "Zde zadejte regulární výrazy, které odpovídají dynamickým originálům. Pro zakázání všech stránek, ponechte prázdné.",
"headers.acac": "Access-Control-Allow-Credentials",
"headers.acam": "Access-Control-Allow-Methods",
"headers.acah": "Access-Control-Allow-Headers",
diff --git a/public/language/en-GB/admin/settings/advanced.json b/public/language/en-GB/admin/settings/advanced.json
index 16eae5a8bd..e4070ab7be 100644
--- a/public/language/en-GB/admin/settings/advanced.json
+++ b/public/language/en-GB/admin/settings/advanced.json
@@ -12,6 +12,10 @@
"headers.acac": "Access-Control-Allow-Credentials",
"headers.acam": "Access-Control-Allow-Methods",
"headers.acah": "Access-Control-Allow-Headers",
+ "hsts": "Strict Transport Security",
+ "hsts.subdomains": "Include subdomains in HSTS header",
+ "hsts.preload": "Allow preloading of HSTS header",
+ "hsts.help": "An HSTS header is already pre-configured for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ",
"traffic-management": "Traffic Management",
"traffic.help": "NodeBB deploys equipped with a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.",
"traffic.enable": "Enable Traffic Management",
diff --git a/public/language/en-GB/groups.json b/public/language/en-GB/groups.json
index 08c8d4d1f5..c18335bb5e 100644
--- a/public/language/en-GB/groups.json
+++ b/public/language/en-GB/groups.json
@@ -33,7 +33,7 @@
"details.grant": "Grant/Rescind Ownership",
"details.kick": "Kick",
"details.kick_confirm": "Are you sure you want to remove this member from the group?",
-
+ "details.add-member": "Add Member",
"details.owner_options": "Group Administration",
"details.group_name": "Group Name",
"details.member_count": "Member Count",
diff --git a/public/language/en-GB/pages.json b/public/language/en-GB/pages.json
index 6780f5cf5d..f6927e63a2 100644
--- a/public/language/en-GB/pages.json
+++ b/public/language/en-GB/pages.json
@@ -6,7 +6,10 @@
"popular-month": "Popular topics this month",
"popular-alltime": "All time popular topics",
"recent": "Recent Topics",
- "top": "Top Voted Topics",
+ "top-day": "Top voted topics today",
+ "top-week": "Top voted topics this week",
+ "top-month": "Top voted topics this month",
+ "top-alltime": "Top Voted Topics",
"moderator-tools": "Moderator Tools",
"flagged-content": "Flagged Content",
"ip-blacklist": "IP Blacklist",
diff --git a/public/language/en-GB/user.json b/public/language/en-GB/user.json
index a6f389b888..d69941be5d 100644
--- a/public/language/en-GB/user.json
+++ b/public/language/en-GB/user.json
@@ -33,6 +33,8 @@
"following": "Following",
"blocks": "Blocks",
"block_toggle": "Toggle Block",
+ "block_user": "Block User",
+ "unblock_user": "Unblock User",
"aboutme": "About me",
"signature": "Signature",
"birthday": "Birthday",
diff --git a/public/language/sk/admin/manage/privileges.json b/public/language/sk/admin/manage/privileges.json
index 51c095211a..8579b29fd6 100644
--- a/public/language/sk/admin/manage/privileges.json
+++ b/public/language/sk/admin/manage/privileges.json
@@ -20,7 +20,7 @@
"edit-posts": "Upraviť príspevky",
"view-edit-history": "Zobraziť históriu úprav",
"delete-posts": "Odstrániť príspevky",
- "view_deleted": "View Deleted Posts",
+ "view_deleted": "Zobraziť odstránené príspevky",
"upvote-posts": "Súhlasné príspevky",
"downvote-posts": "Nesúhlasné príspevky",
"delete-topics": "Odstrániť témy",
diff --git a/public/language/sk/admin/settings/advanced.json b/public/language/sk/admin/settings/advanced.json
index 0d96976d47..b3678dd04a 100644
--- a/public/language/sk/admin/settings/advanced.json
+++ b/public/language/sk/admin/settings/advanced.json
@@ -8,7 +8,7 @@
"headers.acao": "Access-Control-Allow-Origin",
"headers.acao-regex": "Access-Control-Allow-Origin Regular Expression",
"headers.acao-help": "Ak chcete zamietnuť prístup na všetky stránky, nechajte prázdne",
- "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty",
+ "headers.acao-regex-help": "Sem zadajte regulárne výrazy, ktoré zodpovedajú dynamickým originálom. Pre zakázanie všetkých stránok, ponechajte prázdne.",
"headers.acac": "Access-Control-Allow-Credentials",
"headers.acam": "Access-Control-Allow-Methods",
"headers.acah": "Access-Control-Allow-Headers",
diff --git a/public/language/zh-CN/admin/manage/privileges.json b/public/language/zh-CN/admin/manage/privileges.json
index 780d2d40a1..492f826cac 100644
--- a/public/language/zh-CN/admin/manage/privileges.json
+++ b/public/language/zh-CN/admin/manage/privileges.json
@@ -20,7 +20,7 @@
"edit-posts": "修改回复",
"view-edit-history": "查看修改历史",
"delete-posts": "删除回复",
- "view_deleted": "View Deleted Posts",
+ "view_deleted": "查看已删除回复",
"upvote-posts": "顶",
"downvote-posts": "踩",
"delete-topics": "删除主题",
diff --git a/public/language/zh-CN/admin/settings/advanced.json b/public/language/zh-CN/admin/settings/advanced.json
index 2c890a7076..e81c53baf0 100644
--- a/public/language/zh-CN/admin/settings/advanced.json
+++ b/public/language/zh-CN/admin/settings/advanced.json
@@ -6,9 +6,9 @@
"headers.allow-from": "设置 ALLOW-FROM 来放置 NodeBB 于 iFrame 中",
"headers.powered-by": "自定义由 NodeBB 发送的 \"Powered By\" 头部 ",
"headers.acao": "Access-Control-Allow-Origin",
- "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression",
+ "headers.acao-regex": "Access-Control-Allow-Origin 正则表达式",
"headers.acao-help": "要拒绝所有网站,请留空",
- "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty",
+ "headers.acao-regex-help": "输入正则表达式以匹配动态来源。要拒绝所有网站,请留空",
"headers.acac": "Access-Control-Allow-Credentials",
"headers.acam": "Access-Control-Allow-Methods",
"headers.acah": "Access-Control-Allow-Headers",
diff --git a/public/language/zh-TW/admin/general/languages.json b/public/language/zh-TW/admin/general/languages.json
index bdd57849b3..de8ef3e3a4 100644
--- a/public/language/zh-TW/admin/general/languages.json
+++ b/public/language/zh-TW/admin/general/languages.json
@@ -1,6 +1,6 @@
{
- "language-settings": "Language Settings",
- "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.",
- "default-language": "Default Language",
- "auto-detect": "Auto Detect Language Setting for Guests"
+ "language-settings": "語言設定",
+ "description": "所有使用者的預設語言,使用者可以在個人設定內修改",
+ "default-language": "預設語言",
+ "auto-detect": "使用者為訪客時,自動偵測語言設定"
}
\ No newline at end of file
diff --git a/public/language/zh-TW/flags.json b/public/language/zh-TW/flags.json
index 5d8e09a48a..cac2293840 100644
--- a/public/language/zh-TW/flags.json
+++ b/public/language/zh-TW/flags.json
@@ -6,20 +6,20 @@
"no-flags": "Hooray! No flags found.",
"assignee": "Assignee",
"update": "更新",
- "updated": "Updated",
+ "updated": "更新完成",
"target-purged": "The content this flag referred to has been purged and is no longer available.",
"quick-filters": "快速篩選",
"filter-active": "There are one or more filters active in this list of flags",
- "filter-reset": "Remove Filters",
+ "filter-reset": "移除篩選",
"filters": "Filter Options",
"filter-reporterId": "Reporter UID",
"filter-targetUid": "Flagged UID",
"filter-type": "Flag Type",
"filter-type-all": "All Content",
"filter-type-post": "文章",
- "filter-type-user": "User",
- "filter-state": "State",
+ "filter-type-user": "用戶",
+ "filter-state": "狀態",
"filter-assignee": "Assignee UID",
"filter-cid": "分類",
"filter-quick-mine": "Assigned to me",
@@ -32,11 +32,11 @@
"start-new-chat": "Start New Chat",
"go-to-target": "View Flag Target",
- "user-view": "View Profile",
- "user-edit": "Edit Profile",
+ "user-view": "查看個人資料",
+ "user-edit": "編輯個人資料",
- "notes": "Flag Notes",
- "add-note": "Add Note",
+ "notes": "標記備註",
+ "add-note": "新增備註",
"no-notes": "No shared notes.",
"history": "Flag History",
@@ -47,7 +47,7 @@
"state-open": "New/Open",
"state-wip": "Work in Progress",
"state-resolved": "Resolved",
- "state-rejected": "Rejected",
+ "state-rejected": "已拒絕",
"no-assignee": "Not Assigned",
"note-added": "Note Added",
diff --git a/public/src/admin/manage/group.js b/public/src/admin/manage/group.js
index 95e1751ff6..92ef056630 100644
--- a/public/src/admin/manage/group.js
+++ b/public/src/admin/manage/group.js
@@ -5,20 +5,14 @@ define('admin/manage/group', [
'forum/groups/memberlist',
'iconSelect',
'admin/modules/colorpicker',
- 'translator',
- 'benchpress',
-], function (memberList, iconSelect, colorpicker, translator, Benchpress) {
+], function (memberList, iconSelect, colorpicker) {
var Groups = {};
Groups.init = function () {
- var groupDetailsSearch = $('#group-details-search');
- var groupDetailsSearchResults = $('#group-details-search-results');
var groupIcon = $('#group-icon');
var changeGroupUserTitle = $('#change-group-user-title');
var changeGroupLabelColor = $('#change-group-label-color');
var groupLabelPreview = $('#group-label-preview');
- var searchDelay;
-
var groupName = ajaxify.data.group.name;
@@ -36,87 +30,6 @@ define('admin/manage/group', [
groupLabelPreview.css('background', changeGroupLabelColor.val() || '#000000');
});
- groupDetailsSearch.on('keyup', function () {
- if (searchDelay) {
- clearTimeout(searchDelay);
- }
-
- searchDelay = setTimeout(function () {
- var searchText = groupDetailsSearch.val();
- var foundUser;
-
- socket.emit('admin.user.search', {
- query: searchText,
- }, function (err, results) {
- if (!err && results && results.users.length > 0) {
- var numResults = results.users.length;
- var x;
- if (numResults > 20) {
- numResults = 20;
- }
-
- groupDetailsSearchResults.empty();
-
- for (x = 0; x < numResults; x += 1) {
- foundUser = $('
');
- foundUser
- .attr({
- title: results.users[x].username,
- 'data-uid': results.users[x].uid,
- 'data-username': results.users[x].username,
- 'data-userslug': results.users[x].userslug,
- 'data-picture': results.users[x].picture,
- 'data-usericon-bgColor': results.users[x]['icon:bgColor'],
- 'data-usericon-text': results.users[x]['icon:text'],
- })
- .append(results.users[x].picture ?
- $('
').addClass('avatar avatar-sm').attr('src', results.users[x].picture) :
- $('').addClass('avatar avatar-sm').css('background-color', results.users[x]['icon:bgColor']).html(results.users[x]['icon:text']))
- .append($('').html(results.users[x].username));
-
- groupDetailsSearchResults.append(foundUser);
- }
- } else {
- groupDetailsSearchResults.translateHtml('[[admin/manage/groups:edit.no-users-found]]');
- }
- });
- }, 200);
- });
-
- groupDetailsSearchResults.on('click', 'li[data-uid]', function () {
- var userLabel = $(this);
- var uid = parseInt(userLabel.attr('data-uid'), 10);
-
- socket.emit('admin.groups.join', {
- groupName: groupName,
- uid: uid,
- }, function (err) {
- if (err) {
- return app.alertError(err.message);
- }
-
- var member = {
- uid: userLabel.attr('data-uid'),
- username: userLabel.attr('data-username'),
- userslug: userLabel.attr('data-userslug'),
- picture: userLabel.attr('data-picture'),
- 'icon:bgColor': userLabel.attr('data-usericon-bgColor'),
- 'icon:text': userLabel.attr('data-usericon-text'),
- };
-
- Benchpress.parse('admin/partials/groups/memberlist', 'group.members', {
- group: {
- isOwner: ajaxify.data.group.isOwner,
- members: [member],
- },
- }, function (html) {
- translator.translate(html, function (html) {
- $('[component="groups/members"] tbody').prepend(html);
- });
- });
- });
- });
-
$('[component="groups/members"]').on('click', '[data-action]', function () {
var btnEl = $(this);
var userRow = btnEl.parents('[data-uid]');
diff --git a/public/src/client/account/blocks.js b/public/src/client/account/blocks.js
index f6ef1154d0..11d656fa46 100644
--- a/public/src/client/account/blocks.js
+++ b/public/src/client/account/blocks.js
@@ -17,7 +17,8 @@ define('forum/account/blocks', ['forum/account/header', 'autocomplete'], functio
$('.block-edit').on('click', '[data-action="toggle"]', function () {
var uid = parseInt(this.getAttribute('data-uid'), 10);
socket.emit('user.toggleBlock', {
- uid: uid,
+ blockeeUid: uid,
+ blockerUid: ajaxify.data.uid,
}, Blocks.refreshList);
});
};
diff --git a/public/src/client/account/edit.js b/public/src/client/account/edit.js
index 9956c659c6..f7682aae1c 100644
--- a/public/src/client/account/edit.js
+++ b/public/src/client/account/edit.js
@@ -169,10 +169,9 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
confirmBtn.html('');
confirmBtn.prop('disabled', true);
- socket.emit('user.checkPassword', {
- uid: parseInt(ajaxify.data.uid, 10),
+ socket.emit('user.deleteAccount', {
password: $('#confirm-password').val(),
- }, function (err, ok) {
+ }, function (err) {
function restoreButton() {
translator.translate('[[modules:bootbox.confirm]]', function (confirmText) {
confirmBtn.text(confirmText);
@@ -183,19 +182,10 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
if (err) {
restoreButton();
return app.alertError(err.message);
- } else if (!ok) {
- restoreButton();
- return app.alertError('[[error:invalid-password]]');
}
confirmBtn.html('');
- socket.emit('user.deleteAccount', {}, function (err) {
- if (err) {
- return app.alertError(err.message);
- }
-
- window.location.href = config.relative_path + '/';
- });
+ window.location.href = config.relative_path + '/';
});
return false;
diff --git a/public/src/client/account/header.js b/public/src/client/account/header.js
index 1ecacc812d..100256e762 100644
--- a/public/src/client/account/header.js
+++ b/public/src/client/account/header.js
@@ -53,6 +53,7 @@ define('forum/account/header', [
components.get('account/unban').on('click', unbanAccount);
components.get('account/delete').on('click', deleteAccount);
components.get('account/flag').on('click', flagAccount);
+ components.get('account/block').on('click', toggleBlockAccount);
};
function hidePrivateLinks() {
@@ -191,6 +192,25 @@ define('forum/account/header', [
});
}
+ function toggleBlockAccount() {
+ var targetEl = this;
+ socket.emit('user.toggleBlock', {
+ blockeeUid: ajaxify.data.uid,
+ blockerUid: app.user.uid,
+ }, function (err, blocked) {
+ if (err) {
+ return app.alertError(err.message);
+ }
+
+ translator.translate('[[user:' + (blocked ? 'unblock' : 'block') + '_user]]', function (label) {
+ $(targetEl).text(label);
+ });
+ });
+
+ // Keep dropdown open
+ return false;
+ }
+
function removeCover() {
translator.translate('[[user:remove_cover_picture_confirm]]', function (translated) {
bootbox.confirm(translated, function (confirm) {
diff --git a/public/src/client/groups/memberlist.js b/public/src/client/groups/memberlist.js
index cc3b36ce3c..82e2cdc9a7 100644
--- a/public/src/client/groups/memberlist.js
+++ b/public/src/client/groups/memberlist.js
@@ -1,7 +1,7 @@
'use strict';
-define('forum/groups/memberlist', ['components', 'forum/infinitescroll'], function () {
+define('forum/groups/memberlist', ['autocomplete'], function (autocomplete) {
var MemberList = {};
var searchInterval;
var groupName;
@@ -11,10 +11,40 @@ define('forum/groups/memberlist', ['components', 'forum/infinitescroll'], functi
templateName = _templateName || 'groups/details';
groupName = ajaxify.data.group.name;
+ handleMemberAdd();
handleMemberSearch();
handleMemberInfiniteScroll();
};
+ function handleMemberAdd() {
+ $('[component="groups/members/add"]').on('click', function () {
+ var modal = bootbox.dialog({
+ title: '[[groups:details.add-member]]',
+ message: '',
+ });
+ autocomplete.user(modal.find('input'), function (ev, ui) {
+ var user = ui.item.user;
+ if (user) {
+ addUserToGroup(user, function () {
+ modal.modal('hide');
+ });
+ }
+ });
+ });
+ }
+
+ function addUserToGroup(user, callback) {
+ socket.emit('groups.addMember', { groupName: groupName, uid: user.uid }, function (err) {
+ if (err) {
+ return app.alertError(err);
+ }
+ parseAndTranslate([user], function (html) {
+ $('[component="groups/members"] tbody').prepend(html);
+ });
+ callback();
+ });
+ }
+
function handleMemberSearch() {
$('[component="groups/members/search"]').on('keyup', function () {
var query = $(this).val();
diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js
index f2f418a52f..1d2a30272e 100644
--- a/src/controllers/accounts/helpers.js
+++ b/src/controllers/accounts/helpers.js
@@ -91,6 +91,9 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
canBanUser: function (next) {
privileges.users.canBanUser(callerUID, uid, next);
},
+ isBlocked: function (next) {
+ user.blocks.is(uid, callerUID, next);
+ },
}, next);
},
function (results, next) {
@@ -129,6 +132,11 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
userData.moderationNote = undefined;
}
+ userData.isBlocked = results.isBlocked;
+ if (isAdmin || isSelf) {
+ userData.blocksCount = parseInt(userData.blocksCount, 10) || 0;
+ }
+
userData.yourid = callerUID;
userData.theirid = userData.uid;
userData.isTargetAdmin = results.isTargetAdmin;
@@ -165,7 +173,6 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
userData.websiteName = userData.website.replace(validator.escape('http://'), '').replace(validator.escape('https://'), '');
userData.followingCount = parseInt(userData.followingCount, 10) || 0;
userData.followerCount = parseInt(userData.followerCount, 10) || 0;
- userData.blocksCount = parseInt(userData.blocksCount, 10) || 0;
userData.email = validator.escape(String(userData.email || ''));
userData.fullname = validator.escape(String(userData.fullname || ''));
diff --git a/src/controllers/index.js b/src/controllers/index.js
index 052995c60b..a91636dfc7 100644
--- a/src/controllers/index.js
+++ b/src/controllers/index.js
@@ -39,32 +39,41 @@ Controllers.errors = require('./errors');
Controllers.composer = require('./composer');
Controllers.reset = function (req, res, next) {
+ const renderReset = function (code, valid) {
+ res.render('reset_code', {
+ valid: valid,
+ displayExpiryNotice: req.session.passwordExpired,
+ code: code,
+ minimumPasswordLength: parseInt(meta.config.minimumPasswordLength, 10),
+ minimumPasswordStrength: parseInt(meta.config.minimumPasswordStrength, 10),
+ breadcrumbs: helpers.buildBreadcrumbs([
+ {
+ text: '[[reset_password:reset_password]]',
+ url: '/reset',
+ },
+ {
+ text: '[[reset_password:update_password]]',
+ },
+ ]),
+ title: '[[pages:reset]]',
+ });
+ delete req.session.passwordExpired;
+ };
+
if (req.params.code) {
- async.waterfall([
- function (next) {
- user.reset.validate(req.params.code, next);
- },
- function (valid) {
- res.render('reset_code', {
- valid: valid,
- displayExpiryNotice: req.session.passwordExpired,
- code: req.params.code,
- minimumPasswordLength: parseInt(meta.config.minimumPasswordLength, 10),
- minimumPasswordStrength: parseInt(meta.config.minimumPasswordStrength, 10),
- breadcrumbs: helpers.buildBreadcrumbs([
- {
- text: '[[reset_password:reset_password]]',
- url: '/reset',
- },
- {
- text: '[[reset_password:update_password]]',
- },
- ]),
- title: '[[pages:reset]]',
- });
- delete req.session.passwordExpired;
- },
- ], next);
+ // Save to session and redirect
+ req.session.reset_code = req.params.code;
+ res.redirect(nconf.get('relative_path') + '/reset');
+ } else if (req.session.reset_code) {
+ // Validate and save to local variable before removing from session
+ user.reset.validate(req.session.reset_code, function (err, valid) {
+ if (err) {
+ return next(err);
+ }
+
+ renderReset(req.session.reset_code, valid);
+ delete req.session.reset_code;
+ });
} else {
res.render('reset', {
code: null,
diff --git a/src/controllers/popular.js b/src/controllers/popular.js
index d8e7fbb4bf..138fc11817 100644
--- a/src/controllers/popular.js
+++ b/src/controllers/popular.js
@@ -3,7 +3,7 @@
var async = require('async');
var nconf = require('nconf');
-
+var validator = require('validator');
var helpers = require('./helpers');
var recentController = require('./recent');
@@ -26,7 +26,7 @@ popularController.get = function (req, res, next) {
data.breadcrumbs = helpers.buildBreadcrumbs(breadcrumbs);
}
var feedQs = data.rssFeedUrl.split('?')[1];
- data.rssFeedUrl = nconf.get('relative_path') + '/popular/' + (req.query.term || 'alltime') + '.rss';
+ data.rssFeedUrl = nconf.get('relative_path') + '/popular/' + (validator.escape(String(req.query.term)) || 'alltime') + '.rss';
if (req.loggedIn) {
data.rssFeedUrl += '?' + feedQs;
}
diff --git a/src/controllers/recent.js b/src/controllers/recent.js
index e0d037d0f6..334d709784 100644
--- a/src/controllers/recent.js
+++ b/src/controllers/recent.js
@@ -37,7 +37,7 @@ recentController.getData = function (req, url, sort, callback) {
var rssToken;
if (!helpers.validFilters[filter] || (!term && req.query.term)) {
- return callback();
+ return callback(null, null);
}
term = term || 'alltime';
diff --git a/src/controllers/top.js b/src/controllers/top.js
index b471acac60..718d9cfa47 100644
--- a/src/controllers/top.js
+++ b/src/controllers/top.js
@@ -2,6 +2,10 @@
'use strict';
var async = require('async');
+var nconf = require('nconf');
+var validator = require('validator');
+
+var helpers = require('./helpers');
var recentController = require('./recent');
var topController = module.exports;
@@ -15,6 +19,16 @@ topController.get = function (req, res, next) {
if (!data) {
return next();
}
+ var term = helpers.terms[req.query.term] || 'alltime';
+ if (req.originalUrl.startsWith(nconf.get('relative_path') + '/api/top') || req.originalUrl.startsWith(nconf.get('relative_path') + '/top')) {
+ data.title = '[[pages:top-' + term + ']]';
+ }
+
+ var feedQs = data.rssFeedUrl.split('?')[1];
+ data.rssFeedUrl = nconf.get('relative_path') + '/top/' + (validator.escape(String(req.query.term)) || 'alltime') + '.rss';
+ if (req.loggedIn) {
+ data.rssFeedUrl += '?' + feedQs;
+ }
res.render('top', data);
},
], next);
diff --git a/src/meta/minifier.js b/src/meta/minifier.js
index c48bbb890f..14da9beb93 100644
--- a/src/meta/minifier.js
+++ b/src/meta/minifier.js
@@ -112,7 +112,7 @@ if (process.env.minifier_child) {
if (err) {
process.send({
type: 'error',
- message: err.stack,
+ message: err.stack || err.message || 'unknown error',
});
return;
}
@@ -277,7 +277,7 @@ function buildCSS(data, callback) {
from: undefined,
}).then(function (result) {
process.nextTick(callback, null, { code: result.css });
- }, function (err) {
+ }).catch(function (err) {
process.nextTick(callback, err);
});
});
diff --git a/src/meta/themes.js b/src/meta/themes.js
index 0f22993c3f..d011bed707 100644
--- a/src/meta/themes.js
+++ b/src/meta/themes.js
@@ -104,9 +104,9 @@ Themes.get = function (callback) {
// Minor adjustments for API output
configObj.type = 'local';
if (configObj.screenshot) {
- configObj.screenshot_url = 'css/previews/' + encodeURIComponent(configObj.id);
+ configObj.screenshot_url = nconf.get('relative_path') + '/css/previews/' + encodeURIComponent(configObj.id);
} else {
- configObj.screenshot_url = 'assets/images/themes/default.png';
+ configObj.screenshot_url = nconf.get('relative_path') + '/assets/images/themes/default.png';
}
next(null, configObj);
} catch (err) {
diff --git a/src/middleware/user.js b/src/middleware/user.js
index e7c6c7ef36..17c52e7ca6 100644
--- a/src/middleware/user.js
+++ b/src/middleware/user.js
@@ -223,16 +223,4 @@ module.exports = function (middleware) {
return next();
}
};
-
- middleware.handleBlocking = function (req, res, next) {
- user.blocks.is(res.locals.uid, req.uid, function (err, blocked) {
- if (err) {
- return next(err);
- } else if (blocked) {
- res.status(404).render('404', { title: '[[global:404.title]]' });
- } else {
- return next();
- }
- });
- };
};
diff --git a/src/plugins.js b/src/plugins.js
index a4d28944fe..54aa45d49a 100644
--- a/src/plugins.js
+++ b/src/plugins.js
@@ -9,7 +9,6 @@ var express = require('express');
var nconf = require('nconf');
var hotswap = require('./hotswap');
-var file = require('./file');
var app;
var middleware;
@@ -138,56 +137,6 @@ Plugins.reloadRoutes = function (callback) {
});
};
-var themeNamePattern = /^(@.*?\/)?nodebb-theme-.*$/;
-
-// DEPRECATED: remove in v1.8.0
-Plugins.getTemplates = function (callback) {
- var templates = {};
- var tplName;
-
- winston.warn('[deprecated] Plugins.getTemplates is DEPRECATED to be removed in v1.8.0');
-
- Plugins.data.getActive(function (err, plugins) {
- if (err) {
- return callback(err);
- }
-
- async.eachSeries(plugins, function (plugin, next) {
- if (plugin.templates || themeNamePattern.test(plugin.id)) {
- winston.verbose('[plugins] Loading templates (' + plugin.id + ')');
- var templatesPath = path.join(__dirname, '../node_modules', plugin.id, plugin.templates || 'templates');
- file.walk(templatesPath, function (err, pluginTemplates) {
- if (pluginTemplates) {
- pluginTemplates.forEach(function (pluginTemplate) {
- if (pluginTemplate.endsWith('.tpl')) {
- tplName = '/' + pluginTemplate.replace(templatesPath, '').substring(1);
-
- if (templates.hasOwnProperty(tplName)) {
- winston.verbose('[plugins] ' + tplName + ' replaced by ' + plugin.id);
- }
-
- templates[tplName] = pluginTemplate;
- } else {
- winston.warn('[plugins] Skipping ' + pluginTemplate + ' by plugin ' + plugin.id);
- }
- });
- } else if (err) {
- winston.error(err);
- } else {
- winston.warn('[plugins/' + plugin.id + '] A templates directory was defined for this plugin, but was not found.');
- }
-
- next(false);
- });
- } else {
- next(false);
- }
- }, function (err) {
- callback(err, templates);
- });
- });
-};
-
Plugins.get = function (id, callback) {
var url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins/' + id;
@@ -228,6 +177,7 @@ Plugins.list = function (matching, callback) {
};
Plugins.normalise = function (apiReturn, callback) {
+ var themeNamePattern = /^(@.*?\/)?nodebb-theme-.*$/;
var pluginMap = {};
var dependencies = require(path.join(nconf.get('base_dir'), 'package.json')).dependencies;
apiReturn = Array.isArray(apiReturn) ? apiReturn : [];
diff --git a/src/routes/accounts.js b/src/routes/accounts.js
index 56cedb809a..9febb67391 100644
--- a/src/routes/accounts.js
+++ b/src/routes/accounts.js
@@ -4,8 +4,8 @@ var helpers = require('./helpers');
var setupPageRoute = helpers.setupPageRoute;
module.exports = function (app, middleware, controllers) {
- var middlewares = [middleware.checkGlobalPrivacySettings, middleware.exposeUid, middleware.handleBlocking];
- var accountMiddlewares = [middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions, middleware.exposeUid, middleware.handleBlocking];
+ var middlewares = [middleware.checkGlobalPrivacySettings, middleware.exposeUid];
+ var accountMiddlewares = [middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions, middleware.exposeUid];
setupPageRoute(app, '/me/*', middleware, [], middleware.redirectMeToUserslug);
setupPageRoute(app, '/uid/:uid*', middleware, [], middleware.redirectUidToUserslug);
diff --git a/src/routes/feeds.js b/src/routes/feeds.js
index f800ab806d..f843e08e7a 100644
--- a/src/routes/feeds.js
+++ b/src/routes/feeds.js
@@ -16,11 +16,19 @@ var db = require('../database');
var utils = require('../utils');
var controllers404 = require('../controllers/404.js');
+var terms = {
+ daily: 'day',
+ weekly: 'week',
+ monthly: 'month',
+ alltime: 'alltime',
+};
+
module.exports = function (app, middleware) {
app.get('/topic/:topic_id.rss', middleware.maintenanceMode, generateForTopic);
app.get('/category/:category_id.rss', middleware.maintenanceMode, generateForCategory);
app.get('/recent.rss', middleware.maintenanceMode, generateForRecent);
app.get('/top.rss', middleware.maintenanceMode, generateForTop);
+ app.get('/top/:term.rss', middleware.maintenanceMode, generateForTop);
app.get('/popular.rss', middleware.maintenanceMode, generateForPopular);
app.get('/popular/:term.rss', middleware.maintenanceMode, generateForPopular);
app.get('/recentposts.rss', middleware.maintenanceMode, generateForRecentPosts);
@@ -212,7 +220,8 @@ function generateForTop(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
return controllers404.send404(req, res);
}
-
+ var term = terms[req.params.term] || 'day';
+ var uid;
async.waterfall([
function (next) {
if (req.query.token && req.query.uid) {
@@ -222,13 +231,27 @@ function generateForTop(req, res, next) {
}
},
function (token, next) {
- generateForTopics({
- uid: token && token === req.query.token ? req.query.uid : req.uid,
+ uid = token && token === req.query.token ? req.query.uid : req.uid;
+
+ topics.getSortedTopics({
+ uid: uid,
+ start: 0,
+ stop: 19,
+ term: term,
+ sort: 'votes',
+ }, next);
+ },
+ function (result, next) {
+ generateTopicsFeed({
+ uid: uid,
title: 'Top Voted Topics',
description: 'A list of topics that have received the most votes',
- feed_url: '/top.rss',
- site_url: '/top',
- }, 'topics:votes', req, res, next);
+ feed_url: '/top/' + (req.params.term || 'daily') + '.rss',
+ site_url: '/top/' + (req.params.term || 'daily'),
+ }, result.topics, next);
+ },
+ function (feed) {
+ sendFeed(feed, res);
},
], next);
}
@@ -237,12 +260,7 @@ function generateForPopular(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
return controllers404.send404(req, res);
}
- var terms = {
- daily: 'day',
- weekly: 'week',
- monthly: 'month',
- alltime: 'alltime',
- };
+
var term = terms[req.params.term] || 'day';
var uid;
async.waterfall([
diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js
index e98b8451c6..033a4e7a4b 100644
--- a/src/socket.io/groups.js
+++ b/src/socket.io/groups.js
@@ -71,14 +71,27 @@ SocketGroups.leave = function (socket, data, callback) {
groups.leave(data.groupName, socket.uid, callback);
};
+SocketGroups.addMember = isOwner(function (socket, data, callback) {
+ if (data.groupName === 'administrators' || groups.isPrivilegeGroup(data.groupName)) {
+ return callback(new Error('[[error:not-allowed]]'));
+ }
+ groups.join(data.groupName, data.uid, callback);
+});
+
function isOwner(next) {
return function (socket, data, callback) {
async.parallel({
isAdmin: async.apply(user.isAdministrator, socket.uid),
+ isGlobalModerator: async.apply(user.isGlobalModerator, socket.uid),
isOwner: async.apply(groups.ownership.isOwner, socket.uid, data.groupName),
+ group: async.apply(groups.getGroupData, data.groupName),
}, function (err, results) {
- if (err || (!results.isOwner && !results.isAdmin)) {
- return callback(err || new Error('[[error:no-privileges]]'));
+ if (err) {
+ return callback(err);
+ }
+ var isOwner = results.isOwner || results.isAdmin || (results.isGlobalModerator && !results.group.system);
+ if (!isOwner) {
+ return callback(new Error('[[error:no-privileges]]'));
}
next(socket, data, callback);
});
diff --git a/src/socket.io/user.js b/src/socket.io/user.js
index 071cb6a5f7..5401ccca44 100644
--- a/src/socket.io/user.js
+++ b/src/socket.io/user.js
@@ -36,6 +36,11 @@ SocketUser.deleteAccount = function (socket, data, callback) {
}
async.waterfall([
+ function (next) {
+ user.isPasswordCorrect(socket.uid, data.password, function (err, ok) {
+ next(err || !ok ? new Error('[[error:invalid-password]]') : undefined);
+ });
+ },
function (next) {
user.isAdministrator(socket.uid, next);
},
@@ -56,7 +61,15 @@ SocketUser.deleteAccount = function (socket, data, callback) {
});
next();
},
- ], callback);
+ ], function (err) {
+ if (err) {
+ return setTimeout(function () {
+ callback(err);
+ }, 2500);
+ }
+
+ callback();
+ });
};
SocketUser.emailExists = function (socket, data, callback) {
@@ -115,6 +128,7 @@ SocketUser.reset.commit = function (socket, data, callback) {
async.parallel({
uid: async.apply(db.getObjectField, 'reset:uid', data.code),
reset: async.apply(user.reset.commit, data.code, data.password),
+ hook: async.apply(plugins.fireHook, 'action:password.reset', { uid: socket.uid }),
}, next);
},
function (results, next) {
diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js
index 49e63744c6..6cf76be9ee 100644
--- a/src/socket.io/user/profile.js
+++ b/src/socket.io/user/profile.js
@@ -109,13 +109,6 @@ module.exports = function (SocketUser) {
], callback);
}
- SocketUser.checkPassword = function (socket, data, callback) {
- isPrivilegedOrSelfAndPasswordMatch(socket.uid, data, function (err) {
- // Return a bool (without delayed response to prevent brute-force checking of password validity)
- setTimeout(callback.bind(null, null, !err), 1000);
- });
- };
-
SocketUser.changePassword = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:invalid-uid]]'));
@@ -208,19 +201,24 @@ module.exports = function (SocketUser) {
};
SocketUser.toggleBlock = function (socket, data, callback) {
+ let current;
+
async.waterfall([
function (next) {
- user.blocks.can(data.uid, next);
+ user.blocks.can(socket.uid, data.blockerUid, data.blockeeUid, next);
},
function (can, next) {
if (!can) {
return next(new Error('[[error:cannot-block-privileged]]'));
}
- user.blocks.is(data.uid, socket.uid, next);
+ user.blocks.is(data.blockeeUid, data.blockerUid, next);
},
function (is, next) {
- user.blocks[is ? 'remove' : 'add'](data.uid, socket.uid, next);
+ current = is;
+ user.blocks[is ? 'remove' : 'add'](data.blockeeUid, data.blockerUid, next);
},
- ], callback);
+ ], function (err) {
+ callback(err, !current);
+ });
};
};
diff --git a/src/start.js b/src/start.js
index c8b0905b32..427a58830b 100644
--- a/src/start.js
+++ b/src/start.js
@@ -47,7 +47,7 @@ start.start = function () {
var webserver = require('./webserver');
require('./socket.io').init(webserver.server);
- if (nconf.get('isPrimary') === 'true' && !nconf.get('jobsDisabled')) {
+ if (nconf.get('runJobs')) {
require('./notifications').startJobs();
require('./user').startJobs();
}
diff --git a/src/upgrades/1.10.0/hash_recent_ip_addresses.js b/src/upgrades/1.10.0/hash_recent_ip_addresses.js
index 14bb0ee60e..9e13db0252 100644
--- a/src/upgrades/1.10.0/hash_recent_ip_addresses.js
+++ b/src/upgrades/1.10.0/hash_recent_ip_addresses.js
@@ -11,6 +11,7 @@ module.exports = {
name: 'Hash all IP addresses stored in Recent IPs zset',
timestamp: Date.UTC(2017, 5, 22),
method: function (callback) {
+ const progress = this.progress;
var hashed = /[a-f0-9]{32}/;
let hash;
@@ -18,6 +19,7 @@ module.exports = {
async.each(ips, function (set, next) {
// Short circuit if already processed
if (hashed.test(set.value)) {
+ progress.incr();
return setImmediate(next);
}
@@ -26,8 +28,14 @@ module.exports = {
async.series([
async.apply(db.sortedSetAdd, 'ip:recent', set.score, hash),
async.apply(db.sortedSetRemove, 'ip:recent', set.value),
- ], next);
+ ], function (err) {
+ progress.incr();
+ next(err);
+ });
}, next);
- }, { withScores: 1 }, callback);
+ }, {
+ withScores: 1,
+ progress: this.progress,
+ }, callback);
},
};
diff --git a/src/user/blocks.js b/src/user/blocks.js
index 75767e4a12..a3021968e8 100644
--- a/src/user/blocks.js
+++ b/src/user/blocks.js
@@ -1,9 +1,12 @@
'use strict';
var async = require('async');
-var db = require('../database');
var LRU = require('lru-cache');
+
+var db = require('../database');
+var pubsub = require('../pubsub');
+
module.exports = function (User) {
User.blocks = {
_cache: LRU({
@@ -19,9 +22,29 @@ module.exports = function (User) {
});
};
- User.blocks.can = function (uid, callback) {
+ User.blocks.can = function (callerUid, blockerUid, blockeeUid, callback) {
// Administrators and global moderators cannot be blocked
- User.isAdminOrGlobalMod(uid, (err, can) => callback(err, !can));
+ async.waterfall([
+ function (next) {
+ async.parallel({
+ isCallerAdminOrMod: function (next) {
+ User.isAdminOrGlobalMod(callerUid, next);
+ },
+ isBlockeeAdminOrMod: function (next) {
+ User.isAdminOrGlobalMod(blockeeUid, next);
+ },
+ }, next);
+ },
+ function (results, next) {
+ if (results.isBlockeeAdminOrMod) {
+ return callback(null, false);
+ }
+ if (parseInt(callerUid, 10) !== parseInt(blockerUid, 10) && !results.isCallerAdminOrMod) {
+ return callback(null, false);
+ }
+ next(null, true);
+ },
+ ], callback);
};
User.blocks.list = function (uid, callback) {
@@ -35,11 +58,15 @@ module.exports = function (User) {
}
blocked = blocked.map(uid => parseInt(uid, 10)).filter(Boolean);
- User.blocks._cache.set(uid, blocked);
+ User.blocks._cache.set(parseInt(uid, 10), blocked);
callback(null, blocked);
});
};
+ pubsub.on('user:blocks:cache:del', function (uid) {
+ User.blocks._cache.del(uid);
+ });
+
User.blocks.add = function (targetUid, uid, callback) {
async.waterfall([
async.apply(this.applyChecks, true, targetUid, uid),
@@ -47,9 +74,9 @@ module.exports = function (User) {
async.apply(User.incrementUserFieldBy, uid, 'blocksCount', 1),
function (_blank, next) {
User.blocks._cache.del(uid);
+ pubsub.publish('user:blocks:cache:del', uid);
setImmediate(next);
},
- async.apply(User.blocks.list, uid),
], callback);
};
@@ -60,9 +87,9 @@ module.exports = function (User) {
async.apply(User.decrementUserFieldBy, uid, 'blocksCount', 1),
function (_blank, next) {
User.blocks._cache.del(uid);
+ pubsub.publish('user:blocks:cache:del', uid);
setImmediate(next);
},
- async.apply(User.blocks.list, uid),
], callback);
};
diff --git a/src/user/profile.js b/src/user/profile.js
index fc320bd5d0..5da6eb7409 100644
--- a/src/user/profile.js
+++ b/src/user/profile.js
@@ -200,6 +200,7 @@ module.exports = function (User) {
async.series([
async.apply(db.sortedSetRemove, 'email:uid', oldEmail.toLowerCase()),
async.apply(db.sortedSetRemove, 'email:sorted', oldEmail.toLowerCase() + ':' + uid),
+ async.apply(User.auth.revokeAllSessions, uid),
], function (err) {
next(err);
});
@@ -339,6 +340,7 @@ module.exports = function (User) {
}),
async.apply(User.reset.updateExpiry, data.uid),
async.apply(User.auth.revokeAllSessions, data.uid),
+ async.apply(plugins.fireHook, 'action:password.change', { uid: uid }),
], function (err) {
next(err);
});
diff --git a/src/views/admin/manage/group.tpl b/src/views/admin/manage/group.tpl
index b9170df21a..51301f8e56 100644
--- a/src/views/admin/manage/group.tpl
+++ b/src/views/admin/manage/group.tpl
@@ -74,13 +74,6 @@
-
-