Merge commit 'b88a8de6a159d0b2cd5114e084b38c82c24d5a2a' into v1.10.x

This commit is contained in:
Misty (Bot)
2018-07-05 18:42:48 +00:00
45 changed files with 403 additions and 315 deletions

View File

@@ -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",

View File

@@ -20,7 +20,7 @@
"edit-posts": "Редактиране на публикации",
"view-edit-history": "Преглед на историята на редакциите",
"delete-posts": "Изтриване на публикации",
"view_deleted": "View Deleted Posts",
"view_deleted": "Преглед на изтритите публикации",
"upvote-posts": "Положително гласуване за публикации",
"downvote-posts": "Отрицателно гласуване за публикации",
"delete-topics": "Изтриване на теми",

View File

@@ -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": "Заглавки за разрешаване на управлението на достъпа",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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. <a href=\"%1\">More information <i class=\"fa fa-external-link\"></i></a>",
"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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -20,7 +20,7 @@
"edit-posts": "修改回复",
"view-edit-history": "查看修改历史",
"delete-posts": "删除回复",
"view_deleted": "View Deleted Posts",
"view_deleted": "查看已删除回复",
"upvote-posts": "顶",
"downvote-posts": "踩",
"delete-topics": "删除主题",

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"language-settings": "Language Settings",
"description": "The default language determines the language settings for all users who are visiting your forum. <br />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": "使用者為訪客時,自動偵測語言設定"
}

View File

@@ -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",

View File

@@ -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 = $('<li />');
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 ?
$('<img />').addClass('avatar avatar-sm').attr('src', results.users[x].picture) :
$('<div />').addClass('avatar avatar-sm').css('background-color', results.users[x]['icon:bgColor']).html(results.users[x]['icon:text']))
.append($('<span />').html(results.users[x].username));
groupDetailsSearchResults.append(foundUser);
}
} else {
groupDetailsSearchResults.translateHtml('<li>[[admin/manage/groups:edit.no-users-found]]</li>');
}
});
}, 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]');

View File

@@ -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);
});
};

View File

@@ -169,10 +169,9 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
confirmBtn.html('<i class="fa fa-spinner fa-spin"></i>');
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('<i class="fa fa-check"></i>');
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;

View File

@@ -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) {

View File

@@ -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: '<input class="form-control" type="text" placeholder="[[global:search]]"/>',
});
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();

View File

@@ -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 || ''));

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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);

View File

@@ -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);
});
});

View File

@@ -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) {

View File

@@ -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();
}
});
};
};

View File

@@ -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 : [];

View File

@@ -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);

View File

@@ -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([

View File

@@ -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);
});

View File

@@ -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) {

View File

@@ -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);
});
};
};

View File

@@ -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();
}

View File

@@ -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);
},
};

View File

@@ -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);
};

View File

@@ -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);
});

View File

@@ -74,13 +74,6 @@
</div>
</fieldset>
<fieldset>
<label for="add-member">[[admin/manage/groups:edit.add-user]]</label>
<input type="text" class="form-control" id="group-details-search" placeholder="[[admin/manage/groups:edit.add-user-search]]" />
<ul class="members user-list" id="group-details-search-results"></ul>
</fieldset>
<fieldset>
<div class="panel panel-default">
<div class="panel-heading">

View File

@@ -1,7 +1,16 @@
<div class="input-group">
<input class="form-control" type="text" component="groups/members/search" placeholder="[[global:search]]"/>
<span class="input-group-addon search-button"><i class="fa fa-search"></i></span>
</div><br />
<div class="row">
<!-- IF group.isOwner -->
<div class="col-lg-1">
<button component="groups/members/add" type="button" class="btn btn-primary" title="[[groups:details.add-member]]"><i class="fa fa-user-plus"></i></button>
</div>
<!-- ENDIF group.isOwner -->
<div class="<!-- IF group.isOwner -->col-lg-11<!-- ELSE -->col-lg-12<!-- ENDIF group.isOwner -->">
<div class="input-group">
<input class="form-control" type="text" component="groups/members/search" placeholder="[[global:search]]"/>
<span class="input-group-addon search-button"><i class="fa fa-search"></i></span>
</div>
</div>
</div>
<table component="groups/members" class="table table-striped table-hover" data-nextstart="{group.membersNextStart}">
<tbody>
@@ -10,9 +19,9 @@
<td>
<a href="{config.relative_path}/user/{group.members.userslug}">
<!-- IF group.members.picture -->
<img class="avatar avatar-sm" src="{group.members.picture}" />
<img class="avatar avatar-sm avatar-rounded" src="{group.members.picture}" />
<!-- ELSE -->
<div class="avatar avatar-sm" style="background-color: {group.members.icon:bgColor};">{group.members.icon:text}</div>
<div class="avatar avatar-sm avatar-rounded" style="background-color: {group.members.icon:bgColor};">{group.members.icon:text}</div>
<!-- ENDIF group.members.picture -->
</a>
</td>

View File

@@ -1,7 +1,7 @@
<!-- BEGIN themes -->
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12" data-type="{themes.type}" data-theme="{themes.id}"<!-- IF themes.css --> data-css="{themes.css}"<!-- ENDIF themes.css -->>
<div class="theme-card mdl-card mdl-shadow--2dp">
<div class="mdl-card__title mdl-card--expand" style="background-image: url('{relative_path}/{themes.screenshot_url}');"></div>
<div class="mdl-card__title mdl-card--expand" style="background-image: url('{themes.screenshot_url}');"></div>
<div class="mdl-card__supporting-text">
<h2 class="mdl-card__title-text">{themes.name}</h2>
<p>

View File

@@ -63,6 +63,33 @@
</div>
</div>
<div class="row">
<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/advanced:hsts]]</div>
<div class="col-sm-10 col-xs-12">
<form>
<div class="form-group">
<label for="hsts-maxage">[[admin/settings/advanced:hsts.maxAge]]</label>
<input class="form-control" id="hsts-maxage" type="number" placeholder="31536000" data-field="hsts-maxage" /><br />
</div>
<div class="checkbox">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input class="mdl-switch__input" type="checkbox" data-field="hsts-subdomains" checked>
<span class="mdl-switch__label"><strong>[[admin/settings/advanced:hsts.subdomains]]</strong></span>
</label>
</div>
<div class="checkbox">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input class="mdl-switch__input" type="checkbox" data-field="hsts-preload">
<span class="mdl-switch__label"><strong>[[admin/settings/advanced:hsts.preload]]</strong></span>
</label>
</div>
<p class="help-block">
[[admin/settings/advanced:hsts.help, https:\/\/hstspreload.org\/]]
</p>
</form>
</div>
</div>
<div class="row">
<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/advanced:traffic-management]]</div>
<div class="col-sm-10 col-xs-12">

View File

@@ -195,6 +195,11 @@ function setupExpressApp(app, callback) {
app.use(helmet());
app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }));
app.use(helmet.hsts({
maxAge: parseInt(meta.config['hsts-maxage'], 10) || 31536000,
includeSubdomains: !!parseInt(meta.config['hsts-subdomains'], 10),
preload: !!parseInt(meta.config['hsts-preload'], 10),
}));
app.use(middleware.addHeaders);
app.use(middleware.processRender);
auth.initialize(app, middleware);

View File

@@ -2151,7 +2151,6 @@ describe('Controllers', function () {
request(nconf.get('url') + '/api/compose', { json: true }, function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 404);
console.log(body);
plugins.unregisterHook('myTestPlugin', 'filter:composer.build', hookMethod);
done();

View File

@@ -641,30 +641,33 @@ describe('User', function () {
});
it('should update a user\'s profile', function (done) {
var data = {
uid: uid,
username: 'updatedUserName',
email: 'updatedEmail@me.com',
fullname: 'updatedFullname',
website: 'http://nodebb.org',
location: 'izmir',
groupTitle: 'testGroup',
birthday: '01/01/1980',
signature: 'nodebb is good',
};
socketUser.updateProfile({ uid: uid }, data, function (err, result) {
User.create({ username: 'justforupdate', email: 'just@for.updated', password: '123456' }, function (err, uid) {
assert.ifError(err);
assert.equal(result.username, 'updatedUserName');
assert.equal(result.userslug, 'updatedusername');
assert.equal(result.email, 'updatedEmail@me.com');
db.getObject('user:' + uid, function (err, userData) {
var data = {
uid: uid,
username: 'updatedUserName',
email: 'updatedEmail@me.com',
fullname: 'updatedFullname',
website: 'http://nodebb.org',
location: 'izmir',
groupTitle: 'testGroup',
birthday: '01/01/1980',
signature: 'nodebb is good',
};
socketUser.updateProfile({ uid: uid }, data, function (err, result) {
assert.ifError(err);
Object.keys(data).forEach(function (key) {
assert.equal(data[key], userData[key]);
assert.equal(result.username, 'updatedUserName');
assert.equal(result.userslug, 'updatedusername');
assert.equal(result.email, 'updatedEmail@me.com');
db.getObject('user:' + uid, function (err, userData) {
assert.ifError(err);
Object.keys(data).forEach(function (key) {
assert.equal(data[key], userData[key]);
});
done();
});
done();
});
});
});
@@ -699,20 +702,23 @@ describe('User', function () {
assert.ifError(err);
db.getSortedSetRevRange('user:' + uid + ':usernames', 0, -1, function (err, data) {
assert.ifError(err);
assert.equal(data.length, 1);
assert(data[0].startsWith('updatedAgain'));
assert(data[1].startsWith('updatedUserName'));
done();
});
});
});
it('should change email', function (done) {
socketUser.changeUsernameEmail({ uid: uid }, { uid: uid, email: 'updatedAgain@me.com', password: '123456' }, function (err) {
User.create({ username: 'pooremailupdate', email: 'poor@update.me', password: '123456' }, function (err, uid) {
assert.ifError(err);
db.getObjectField('user:' + uid, 'email', function (err, email) {
socketUser.changeUsernameEmail({ uid: uid }, { uid: uid, email: 'updatedAgain@me.com', password: '123456' }, function (err) {
assert.ifError(err);
assert.equal(email, 'updatedAgain@me.com');
done();
db.getObjectField('user:' + uid, 'email', function (err, email) {
assert.ifError(err);
assert.equal(email, 'updatedAgain@me.com');
done();
});
});
});
});
@@ -1791,14 +1797,41 @@ describe('User', function () {
});
});
describe('.toggle()', function () {
it('should toggle block', function (done) {
socketUser.toggleBlock({ uid: 1 }, { blockerUid: 1, blockeeUid: blockeeUid }, function (err) {
assert.ifError(err);
User.blocks.is(blockeeUid, 1, function (err, blocked) {
assert.ifError(err);
assert(blocked);
done();
});
});
});
it('should toggle block', function (done) {
socketUser.toggleBlock({ uid: 1 }, { blockerUid: 1, blockeeUid: blockeeUid }, function (err) {
assert.ifError(err);
User.blocks.is(blockeeUid, 1, function (err, blocked) {
assert.ifError(err);
assert(!blocked);
done();
});
});
});
});
describe('.add()', function () {
it('should block a uid', function (done) {
User.blocks.add(blockeeUid, 1, function (err, blocked_uids) {
User.blocks.add(blockeeUid, 1, function (err) {
assert.ifError(err);
assert.strictEqual(Array.isArray(blocked_uids), true);
assert.strictEqual(blocked_uids.length, 1);
assert.strictEqual(blocked_uids.includes(blockeeUid), true);
done();
User.blocks.list(1, function (err, blocked_uids) {
assert.ifError(err);
assert.strictEqual(Array.isArray(blocked_uids), true);
assert.strictEqual(blocked_uids.length, 1);
assert.strictEqual(blocked_uids.includes(blockeeUid), true);
done();
});
});
});
@@ -1820,11 +1853,14 @@ describe('User', function () {
describe('.remove()', function () {
it('should unblock a uid', function (done) {
User.blocks.remove(blockeeUid, 1, function (err, blocked_uids) {
User.blocks.remove(blockeeUid, 1, function (err) {
assert.ifError(err);
assert.strictEqual(Array.isArray(blocked_uids), true);
assert.strictEqual(blocked_uids.length, 0);
done();
User.blocks.list(1, function (err, blocked_uids) {
assert.ifError(err);
assert.strictEqual(Array.isArray(blocked_uids), true);
assert.strictEqual(blocked_uids.length, 0);
done();
});
});
});
@@ -1929,6 +1965,14 @@ describe('User', function () {
done();
});
});
it('should filter uids that are blocking targetUid', function (done) {
User.blocks.filterUids(blockeeUid, [1, 2], function (err, filtered) {
assert.ifError(err);
assert.deepEqual(filtered, [2]);
done();
});
});
});
});