performance improvements (#8795)

* perf: nconf/winston/render

cache nconf.get calls
modify middleware.pageView to call next earlier
don't call winston.verbose on every hook see https://github.com/winstonjs/winston/issues/1669
translate header/footer separately and cache results for guests

* fix: copy paste fail

* refactor: style and fire hook only log in dev mode

* fix: cache key, header changes based on template

* perf: change replace

* fix: add missing await

* perf: category

* perf: lodash clone

* perf: remove escapeRegexChars
v1.18.x
Barış Soner Uşaklı 5 years ago committed by GitHub
parent 822c13f199
commit a05905f196
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -493,7 +493,10 @@
* @returns {string} * @returns {string}
*/ */
Translator.unescape = function unescape(text) { Translator.unescape = function unescape(text) {
return typeof text === 'string' ? text.replace(/[|\\\[/g, '[').replace(/]|\\\]/g, ']') : text; return typeof text === 'string' ?
text.replace(/[/g, '[').replace(/\\\[/g, '[')
.replace(/]/g, ']').replace(/\\\]/g, ']') :
text;
}; };
/** /**

@ -324,9 +324,7 @@
}, },
// https://github.com/jprichardson/string.js/blob/master/lib/string.js // https://github.com/jprichardson/string.js/blob/master/lib/string.js
stripHTMLTags: function (str, tags) { stripHTMLTags: function (str, tags) {
var pattern = (tags || ['']).map(function (tag) { var pattern = (tags || ['']).join('|');
return utils.escapeRegexChars(tag);
}).join('|');
return String(str).replace(new RegExp('<(\\/)?(' + (pattern || '[^\\s>]+') + ')(\\s+[^<>]*?)?\\s*(\\/)?>', 'gi'), ''); return String(str).replace(new RegExp('<(\\/)?(' + (pattern || '[^\\s>]+') + ')(\\s+[^<>]*?)?\\s*(\\/)?>', 'gi'), '');
}, },

@ -14,6 +14,8 @@ const meta = require('./meta');
const Analytics = module.exports; const Analytics = module.exports;
const secret = nconf.get('secret');
const counters = {}; const counters = {};
let pageViews = 0; let pageViews = 0;
@ -64,10 +66,10 @@ Analytics.pageView = async function (payload) {
if (payload.ip) { if (payload.ip) {
// Retrieve hash or calculate if not present // Retrieve hash or calculate if not present
let hash = ipCache.get(payload.ip + nconf.get('secret')); let hash = ipCache.get(payload.ip + secret);
if (!hash) { if (!hash) {
hash = crypto.createHash('sha1').update(payload.ip + nconf.get('secret')).digest('hex'); hash = crypto.createHash('sha1').update(payload.ip + secret).digest('hex');
ipCache.set(payload.ip + nconf.get('secret'), hash); ipCache.set(payload.ip + secret, hash);
} }
const score = await db.sortedSetScore('ip:recent', hash); const score = await db.sortedSetScore('ip:recent', hash);

@ -157,7 +157,7 @@ Categories.getTagWhitelist = async function (cids) {
}); });
if (!nonCachedCids.length) { if (!nonCachedCids.length) {
return _.clone(cids.map(cid => cachedData[cid])); return cids.map(cid => cachedData[cid]);
} }
const keys = nonCachedCids.map(cid => 'cid:' + cid + ':tag:whitelist'); const keys = nonCachedCids.map(cid => 'cid:' + cid + ':tag:whitelist');
@ -167,7 +167,7 @@ Categories.getTagWhitelist = async function (cids) {
cachedData[cid] = data[index]; cachedData[cid] = data[index];
cache.set('cid:' + cid + ':tag:whitelist', data[index]); cache.set('cid:' + cid + ':tag:whitelist', data[index]);
}); });
return _.clone(cids.map(cid => cachedData[cid])); return cids.map(cid => cachedData[cid]);
}; };
function calculateTopicPostCount(category) { function calculateTopicPostCount(category) {
@ -280,7 +280,7 @@ Categories.getTree = function (categories, parentCid) {
if (cid) { if (cid) {
categories[index].children = undefined; categories[index].children = undefined;
cidToCategory[cid] = categories[index]; cidToCategory[cid] = categories[index];
parents[cid] = _.clone(categories[index]); parents[cid] = { ...categories[index] };
} }
}); });

@ -1,7 +1,5 @@
'use strict'; 'use strict';
const _ = require('lodash');
const db = require('../database'); const db = require('../database');
const topics = require('../topics'); const topics = require('../topics');
const plugins = require('../plugins'); const plugins = require('../plugins');
@ -25,7 +23,7 @@ module.exports = function (Categories) {
}; };
Categories.getTopicIds = async function (data) { Categories.getTopicIds = async function (data) {
const dataForPinned = _.cloneDeep(data); const dataForPinned = { ...data };
dataForPinned.start = 0; dataForPinned.start = 0;
dataForPinned.stop = -1; dataForPinned.stop = -1;

@ -15,11 +15,17 @@ const languages = require('../languages');
const apiController = module.exports; const apiController = module.exports;
const relative_path = nconf.get('relative_path');
const upload_url = nconf.get('upload_url');
const socketioTransports = nconf.get('socket.io:transports') || ['polling', 'websocket'];
const socketioOrigins = nconf.get('socket.io:origins');
const websocketAddress = nconf.get('socket.io:address') || '';
apiController.loadConfig = async function (req) { apiController.loadConfig = async function (req) {
let config = { let config = {
relative_path: nconf.get('relative_path'), relative_path,
upload_url: nconf.get('upload_url'), upload_url,
assetBaseUrl: `${nconf.get('relative_path')}/assets`, assetBaseUrl: `${relative_path}/assets`,
siteTitle: validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB')), siteTitle: validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB')),
browserTitle: validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')), browserTitle: validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')),
titleLayout: (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '&#123;').replace(/}/g, '&#125;'), titleLayout: (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '&#123;').replace(/}/g, '&#125;'),
@ -40,9 +46,9 @@ apiController.loadConfig = async function (req) {
disableChat: meta.config.disableChat === 1, disableChat: meta.config.disableChat === 1,
disableChatMessageEditing: meta.config.disableChatMessageEditing === 1, disableChatMessageEditing: meta.config.disableChatMessageEditing === 1,
maximumChatMessageLength: meta.config.maximumChatMessageLength || 1000, maximumChatMessageLength: meta.config.maximumChatMessageLength || 1000,
socketioTransports: nconf.get('socket.io:transports') || ['polling', 'websocket'], socketioTransports,
socketioOrigins: nconf.get('socket.io:origins'), socketioOrigins,
websocketAddress: nconf.get('socket.io:address') || '', websocketAddress,
maxReconnectionAttempts: meta.config.maxReconnectionAttempts || 5, maxReconnectionAttempts: meta.config.maxReconnectionAttempts || 5,
reconnectionDelay: meta.config.reconnectionDelay || 1500, reconnectionDelay: meta.config.reconnectionDelay || 1500,
topicsPerPage: meta.config.topicsPerPage || 20, topicsPerPage: meta.config.topicsPerPage || 20,

@ -16,6 +16,9 @@ const analytics = require('../analytics');
const categoryController = module.exports; const categoryController = module.exports;
const url = nconf.get('url');
const relative_path = nconf.get('relative_path');
categoryController.get = async function (req, res, next) { categoryController.get = async function (req, res, next) {
const cid = req.params.category_id; const cid = req.params.category_id;
@ -103,7 +106,7 @@ categoryController.get = async function (req, res, next) {
categoryData.showSelect = userPrivileges.editable; categoryData.showSelect = userPrivileges.editable;
categoryData.showTopicTools = userPrivileges.editable; categoryData.showTopicTools = userPrivileges.editable;
categoryData.topicIndex = topicIndex; categoryData.topicIndex = topicIndex;
categoryData.rssFeedUrl = nconf.get('url') + '/category/' + categoryData.cid + '.rss'; categoryData.rssFeedUrl = url + '/category/' + categoryData.cid + '.rss';
if (parseInt(req.uid, 10)) { if (parseInt(req.uid, 10)) {
categories.markAsRead([cid], req.uid); categories.markAsRead([cid], req.uid);
categoryData.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken; categoryData.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken;
@ -115,7 +118,7 @@ categoryController.get = async function (req, res, next) {
categoryData['reputation:disabled'] = meta.config['reputation:disabled']; categoryData['reputation:disabled'] = meta.config['reputation:disabled'];
categoryData.pagination = pagination.create(currentPage, pageCount, req.query); categoryData.pagination = pagination.create(currentPage, pageCount, req.query);
categoryData.pagination.rel.forEach(function (rel) { categoryData.pagination.rel.forEach(function (rel) {
rel.href = nconf.get('url') + '/category/' + categoryData.slug + rel.href; rel.href = url + '/category/' + categoryData.slug + rel.href;
res.locals.linkTags.push(rel); res.locals.linkTags.push(rel);
}); });
@ -128,12 +131,12 @@ async function buildBreadcrumbs(req, categoryData) {
const breadcrumbs = [ const breadcrumbs = [
{ {
text: categoryData.name, text: categoryData.name,
url: nconf.get('relative_path') + '/category/' + categoryData.slug, url: relative_path + '/category/' + categoryData.slug,
cid: categoryData.cid, cid: categoryData.cid,
}, },
]; ];
const crumbs = await helpers.buildCategoryBreadcrumbs(categoryData.parentCid); const crumbs = await helpers.buildCategoryBreadcrumbs(categoryData.parentCid);
if (req.originalUrl.startsWith(nconf.get('relative_path') + '/api/category') || req.originalUrl.startsWith(nconf.get('relative_path') + '/category')) { if (req.originalUrl.startsWith(relative_path + '/api/category') || req.originalUrl.startsWith(relative_path + '/category')) {
categoryData.breadcrumbs = crumbs.concat(breadcrumbs); categoryData.breadcrumbs = crumbs.concat(breadcrumbs);
} }
} }
@ -160,7 +163,7 @@ function addTags(categoryData, res) {
if (categoryData.backgroundImage) { if (categoryData.backgroundImage) {
if (!categoryData.backgroundImage.startsWith('http')) { if (!categoryData.backgroundImage.startsWith('http')) {
categoryData.backgroundImage = nconf.get('url') + categoryData.backgroundImage; categoryData.backgroundImage = url + categoryData.backgroundImage;
} }
res.locals.metaTags.push({ res.locals.metaTags.push({
property: 'og:image', property: 'og:image',
@ -171,7 +174,7 @@ function addTags(categoryData, res) {
res.locals.linkTags = [ res.locals.linkTags = [
{ {
rel: 'up', rel: 'up',
href: nconf.get('url'), href: url,
}, },
]; ];

@ -14,6 +14,9 @@ const middleware = require('../middleware');
const helpers = module.exports; const helpers = module.exports;
const relative_path = nconf.get('relative_path');
const url = nconf.get('url');
helpers.noScriptErrors = async function (req, res, error, httpStatus) { helpers.noScriptErrors = async function (req, res, error, httpStatus) {
if (req.body.noscript !== 'true') { if (req.body.noscript !== 'true') {
return res.status(httpStatus).send(error); return res.status(httpStatus).send(error);
@ -37,7 +40,7 @@ helpers.terms = {
}; };
helpers.buildQueryString = function (query, key, value) { helpers.buildQueryString = function (query, key, value) {
const queryObj = _.clone(query); const queryObj = { ...query };
if (value) { if (value) {
queryObj[key] = value; queryObj[key] = value;
} else { } else {
@ -51,11 +54,11 @@ helpers.addLinkTags = function (params) {
params.res.locals.linkTags = params.res.locals.linkTags || []; params.res.locals.linkTags = params.res.locals.linkTags || [];
params.res.locals.linkTags.push({ params.res.locals.linkTags.push({
rel: 'canonical', rel: 'canonical',
href: nconf.get('url') + '/' + params.url, href: url + '/' + params.url,
}); });
params.tags.forEach(function (rel) { params.tags.forEach(function (rel) {
rel.href = nconf.get('url') + '/' + params.url + rel.href; rel.href = url + '/' + params.url + rel.href;
params.res.locals.linkTags.push(rel); params.res.locals.linkTags.push(rel);
}); });
}; };
@ -136,7 +139,7 @@ helpers.notAllowed = async function (req, res, error) {
helpers.formatApiResponse(401, res, error); helpers.formatApiResponse(401, res, error);
} else { } else {
req.session.returnTo = req.url; req.session.returnTo = req.url;
res.redirect(nconf.get('relative_path') + '/login'); res.redirect(relative_path + '/login');
} }
}; };
@ -144,7 +147,7 @@ helpers.redirect = function (res, url, permanent) {
if (res.locals.isAPI) { if (res.locals.isAPI) {
res.set('X-Redirect', encodeURI(url)).status(200).json(url); res.set('X-Redirect', encodeURI(url)).status(200).json(url);
} else { } else {
res.redirect(permanent ? 308 : 307, nconf.get('relative_path') + encodeURI(url)); res.redirect(permanent ? 308 : 307, relative_path + encodeURI(url));
} }
}; };
@ -157,7 +160,7 @@ helpers.buildCategoryBreadcrumbs = async function (cid) {
if (!data.disabled && !data.isSection) { if (!data.disabled && !data.isSection) {
breadcrumbs.unshift({ breadcrumbs.unshift({
text: String(data.name), text: String(data.name),
url: nconf.get('relative_path') + '/category/' + data.slug, url: relative_path + '/category/' + data.slug,
cid: cid, cid: cid,
}); });
} }
@ -166,13 +169,13 @@ helpers.buildCategoryBreadcrumbs = async function (cid) {
if (meta.config.homePageRoute && meta.config.homePageRoute !== 'categories') { if (meta.config.homePageRoute && meta.config.homePageRoute !== 'categories') {
breadcrumbs.unshift({ breadcrumbs.unshift({
text: '[[global:header.categories]]', text: '[[global:header.categories]]',
url: nconf.get('relative_path') + '/categories', url: relative_path + '/categories',
}); });
} }
breadcrumbs.unshift({ breadcrumbs.unshift({
text: '[[global:home]]', text: '[[global:home]]',
url: nconf.get('relative_path') + '/', url: relative_path + '/',
}); });
return breadcrumbs; return breadcrumbs;
@ -182,14 +185,14 @@ helpers.buildBreadcrumbs = function (crumbs) {
const breadcrumbs = [ const breadcrumbs = [
{ {
text: '[[global:home]]', text: '[[global:home]]',
url: nconf.get('relative_path') + '/', url: relative_path + '/',
}, },
]; ];
crumbs.forEach(function (crumb) { crumbs.forEach(function (crumb) {
if (crumb) { if (crumb) {
if (crumb.url) { if (crumb.url) {
crumb.url = nconf.get('relative_path') + crumb.url; crumb.url = relative_path + crumb.url;
} }
breadcrumbs.push(crumb); breadcrumbs.push(crumb);
} }

@ -14,6 +14,10 @@ const analytics = require('../analytics');
const topicsController = module.exports; const topicsController = module.exports;
const url = nconf.get('url');
const relative_path = nconf.get('relative_path');
const upload_url = nconf.get('upload_url');
topicsController.get = async function getTopic(req, res, callback) { topicsController.get = async function getTopic(req, res, callback) {
const tid = req.params.topic_id; const tid = req.params.topic_id;
@ -79,7 +83,7 @@ topicsController.get = async function getTopic(req, res, callback) {
topicData.scrollToMyPost = settings.scrollToMyPost; topicData.scrollToMyPost = settings.scrollToMyPost;
topicData.allowMultipleBadges = meta.config.allowMultipleBadges === 1; topicData.allowMultipleBadges = meta.config.allowMultipleBadges === 1;
topicData.privateUploads = meta.config.privateUploads === 1; topicData.privateUploads = meta.config.privateUploads === 1;
topicData.rssFeedUrl = nconf.get('relative_path') + '/topic/' + topicData.tid + '.rss'; topicData.rssFeedUrl = relative_path + '/topic/' + topicData.tid + '.rss';
if (req.loggedIn) { if (req.loggedIn) {
topicData.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken; topicData.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken;
} }
@ -96,7 +100,7 @@ topicsController.get = async function getTopic(req, res, callback) {
topicData.pagination = pagination.create(currentPage, pageCount, req.query); topicData.pagination = pagination.create(currentPage, pageCount, req.query);
topicData.pagination.rel.forEach(function (rel) { topicData.pagination.rel.forEach(function (rel) {
rel.href = nconf.get('url') + '/topic/' + topicData.slug + rel.href; rel.href = url + '/topic/' + topicData.slug + rel.href;
res.locals.linkTags.push(rel); res.locals.linkTags.push(rel);
}); });
@ -147,7 +151,7 @@ async function buildBreadcrumbs(topicData) {
const breadcrumbs = [ const breadcrumbs = [
{ {
text: topicData.category.name, text: topicData.category.name,
url: nconf.get('relative_path') + '/category/' + topicData.category.slug, url: relative_path + '/category/' + topicData.category.slug,
cid: topicData.category.cid, cid: topicData.category.cid,
}, },
{ {
@ -211,7 +215,7 @@ async function addTags(topicData, req, res) {
res.locals.linkTags = [ res.locals.linkTags = [
{ {
rel: 'canonical', rel: 'canonical',
href: nconf.get('url') + '/topic/' + topicData.slug, href: url + '/topic/' + topicData.slug,
}, },
]; ];
@ -226,7 +230,7 @@ async function addTags(topicData, req, res) {
if (topicData.category) { if (topicData.category) {
res.locals.linkTags.push({ res.locals.linkTags.push({
rel: 'up', rel: 'up',
href: nconf.get('url') + '/category/' + topicData.category.slug, href: url + '/category/' + topicData.category.slug,
}); });
} }
} }
@ -234,7 +238,7 @@ async function addTags(topicData, req, res) {
async function addOGImageTags(res, topicData, postAtIndex) { async function addOGImageTags(res, topicData, postAtIndex) {
const uploads = postAtIndex ? await posts.uploads.listWithSizes(postAtIndex.pid) : []; const uploads = postAtIndex ? await posts.uploads.listWithSizes(postAtIndex.pid) : [];
const images = uploads.map((upload) => { const images = uploads.map((upload) => {
upload.name = nconf.get('url') + nconf.get('upload_url') + '/files/' + upload.name; upload.name = url + upload_url + '/files/' + upload.name;
return upload; return upload;
}); });
if (topicData.thumb) { if (topicData.thumb) {
@ -252,7 +256,7 @@ async function addOGImageTags(res, topicData, postAtIndex) {
function addOGImageTag(res, image) { function addOGImageTag(res, image) {
let imageUrl; let imageUrl;
if (typeof image === 'string' && !image.startsWith('http')) { if (typeof image === 'string' && !image.startsWith('http')) {
imageUrl = nconf.get('url') + image.replace(new RegExp('^' + nconf.get('relative_path')), ''); imageUrl = url + image.replace(new RegExp('^' + relative_path), '');
} else if (typeof image === 'object') { } else if (typeof image === 'object') {
imageUrl = image.name; imageUrl = image.name;
} else { } else {
@ -327,7 +331,7 @@ topicsController.pagination = async function (req, res, callback) {
const paginationData = pagination.create(currentPage, pageCount); const paginationData = pagination.create(currentPage, pageCount);
paginationData.rel.forEach(function (rel) { paginationData.rel.forEach(function (rel) {
rel.href = nconf.get('url') + '/topic/' + topic.slug + rel.href; rel.href = url + '/topic/' + topic.slug + rel.href;
}); });
res.json({ pagination: paginationData }); res.json({ pagination: paginationData });

@ -4,6 +4,8 @@
const nconf = require('nconf'); const nconf = require('nconf');
const meta = require('./meta'); const meta = require('./meta');
const relative_path = nconf.get('relative_path');
const coverPhoto = module.exports; const coverPhoto = module.exports;
coverPhoto.getDefaultGroupCover = function (groupName) { coverPhoto.getDefaultGroupCover = function (groupName) {
@ -15,7 +17,7 @@ coverPhoto.getDefaultProfileCover = function (uid) {
}; };
function getCover(type, id) { function getCover(type, id) {
const defaultCover = nconf.get('relative_path') + '/assets/images/cover-default.png'; const defaultCover = relative_path + '/assets/images/cover-default.png';
if (meta.config[type + ':defaultCovers']) { if (meta.config[type + ':defaultCovers']) {
const covers = String(meta.config[type + ':defaultCovers']).trim().split(/[\s,]+/g); const covers = String(meta.config[type + ':defaultCovers']).trim().split(/[\s,]+/g);
let coverPhoto = defaultCover; let coverPhoto = defaultCover;
@ -29,7 +31,7 @@ function getCover(type, id) {
id %= covers.length; id %= covers.length;
} }
if (covers[id]) { if (covers[id]) {
coverPhoto = covers[id].startsWith('http') ? covers[id] : (nconf.get('relative_path') + covers[id]); coverPhoto = covers[id].startsWith('http') ? covers[id] : (relative_path + covers[id]);
} }
return coverPhoto; return coverPhoto;
} }

@ -1,9 +1,8 @@
'use strict'; 'use strict';
module.exports = function (module) { module.exports = function (module) {
var helpers = require('./helpers'); const helpers = require('./helpers');
var _ = require('lodash');
const cache = require('../cache').create('mongo'); const cache = require('../cache').create('mongo');
module.objectCache = cache; module.objectCache = cache;
@ -17,7 +16,7 @@ module.exports = function (module) {
const writeData = helpers.serializeData(data); const writeData = helpers.serializeData(data);
try { try {
if (isArray) { if (isArray) {
var bulk = module.client.collection('objects').initializeUnorderedBulkOp(); const bulk = module.client.collection('objects').initializeUnorderedBulkOp();
key.forEach(key => bulk.find({ _key: key }).upsert().updateOne({ $set: writeData })); key.forEach(key => bulk.find({ _key: key }).upsert().updateOne({ $set: writeData }));
await bulk.execute(); await bulk.execute();
} else { } else {
@ -85,12 +84,26 @@ module.exports = function (module) {
return []; return [];
} }
const cachedData = {}; const cachedData = {};
function returnData() { const unCachedKeys = cache.getUnCachedKeys(keys, cachedData);
var mapped = keys.map(function (key) { let data = [];
if (!fields.length) { if (unCachedKeys.length >= 1) {
return _.clone(cachedData[key]); data = await module.client.collection('objects').find(
{ _key: unCachedKeys.length === 1 ? unCachedKeys[0] : { $in: unCachedKeys } },
{ projection: { _id: 0 } }
).toArray();
data = data.map(helpers.deserializeData);
} }
const map = helpers.toMap(data);
unCachedKeys.forEach(function (key) {
cachedData[key] = map[key] || null;
cache.set(key, cachedData[key]);
});
if (!fields.length) {
return keys.map(key => (cachedData[key] ? { ...cachedData[key] } : null));
}
return keys.map(function (key) {
const item = cachedData[key] || {}; const item = cachedData[key] || {};
const result = {}; const result = {};
fields.forEach((field) => { fields.forEach((field) => {
@ -98,29 +111,6 @@ module.exports = function (module) {
}); });
return result; return result;
}); });
return mapped;
}
const unCachedKeys = cache.getUnCachedKeys(keys, cachedData);
if (!unCachedKeys.length) {
return returnData();
}
var query = { _key: { $in: unCachedKeys } };
if (unCachedKeys.length === 1) {
query._key = unCachedKeys[0];
}
let data = await module.client.collection('objects').find(query, { projection: { _id: 0 } }).toArray();
data = data.map(helpers.deserializeData);
var map = helpers.toMap(data);
unCachedKeys.forEach(function (key) {
cachedData[key] = map[key] || null;
cache.set(key, cachedData[key]);
});
return returnData();
}; };
module.getObjectKeys = async function (key) { module.getObjectKeys = async function (key) {

@ -1,9 +1,7 @@
'use strict'; 'use strict';
module.exports = function (module) { module.exports = function (module) {
var helpers = require('./helpers'); const helpers = require('./helpers');
const _ = require('lodash');
const cache = require('../cache').create('redis'); const cache = require('../cache').create('redis');
@ -110,11 +108,10 @@ module.exports = function (module) {
cache.set(key, cachedData[key]); cache.set(key, cachedData[key]);
}); });
const mapped = keys.map(function (key) {
if (!fields.length) { if (!fields.length) {
return _.clone(cachedData[key]); return keys.map(key => (cachedData[key] ? { ...cachedData[key] } : null));
} }
return keys.map(function (key) {
const item = cachedData[key] || {}; const item = cachedData[key] || {};
const result = {}; const result = {};
fields.forEach((field) => { fields.forEach((field) => {
@ -122,7 +119,6 @@ module.exports = function (module) {
}); });
return result; return result;
}); });
return mapped;
}; };
module.getObjectKeys = async function (key) { module.getObjectKeys = async function (key) {

@ -1,6 +1,5 @@
'use strict'; 'use strict';
const _ = require('lodash');
const winston = require('winston'); const winston = require('winston');
const validator = require('validator'); const validator = require('validator');
const cronJob = require('cron').CronJob; const cronJob = require('cron').CronJob;
@ -18,7 +17,7 @@ new cronJob('0 * * * * *', function () {
Errors.writeData = async function () { Errors.writeData = async function () {
try { try {
const _counters = _.clone(counters); const _counters = { ...counters };
counters = {}; counters = {};
const keys = Object.keys(_counters); const keys = Object.keys(_counters);
if (!keys.length) { if (!keys.length) {

@ -9,6 +9,10 @@ const utils = require('../utils');
const Tags = module.exports; const Tags = module.exports;
const url = nconf.get('url');
const relative_path = nconf.get('relative_path');
const upload_url = nconf.get('upload_url');
Tags.parse = async (req, data, meta, link) => { Tags.parse = async (req, data, meta, link) => {
// Meta tags // Meta tags
const defaultTags = [{ const defaultTags = [{
@ -29,7 +33,7 @@ Tags.parse = async (req, data, meta, link) => {
content: Meta.config.title || 'NodeBB', content: Meta.config.title || 'NodeBB',
}, { }, {
name: 'msapplication-badge', name: 'msapplication-badge',
content: 'frequency=30; polling-uri=' + nconf.get('url') + '/sitemap.xml', content: 'frequency=30; polling-uri=' + url + '/sitemap.xml',
noEscape: true, noEscape: true,
}, { }, {
name: 'theme-color', name: 'theme-color',
@ -55,10 +59,10 @@ Tags.parse = async (req, data, meta, link) => {
var defaultLinks = [{ var defaultLinks = [{
rel: 'icon', rel: 'icon',
type: 'image/x-icon', type: 'image/x-icon',
href: nconf.get('relative_path') + '/favicon.ico' + (Meta.config['cache-buster'] ? '?' + Meta.config['cache-buster'] : ''), href: relative_path + '/favicon.ico' + (Meta.config['cache-buster'] ? '?' + Meta.config['cache-buster'] : ''),
}, { }, {
rel: 'manifest', rel: 'manifest',
href: nconf.get('relative_path') + '/manifest.webmanifest', href: relative_path + '/manifest.webmanifest',
}]; }];
if (plugins.hasListeners('filter:search.query')) { if (plugins.hasListeners('filter:search.query')) {
@ -66,7 +70,7 @@ Tags.parse = async (req, data, meta, link) => {
rel: 'search', rel: 'search',
type: 'application/opensearchdescription+xml', type: 'application/opensearchdescription+xml',
title: utils.escapeHTML(String(Meta.config.title || Meta.config.browserTitle || 'NodeBB')), title: utils.escapeHTML(String(Meta.config.title || Meta.config.browserTitle || 'NodeBB')),
href: nconf.get('relative_path') + '/osd.xml', href: relative_path + '/osd.xml',
}); });
} }
@ -74,64 +78,64 @@ Tags.parse = async (req, data, meta, link) => {
if (Meta.config['brand:touchIcon']) { if (Meta.config['brand:touchIcon']) {
defaultLinks.push({ defaultLinks.push({
rel: 'apple-touch-icon', rel: 'apple-touch-icon',
href: nconf.get('relative_path') + nconf.get('upload_url') + '/system/touchicon-orig.png', href: relative_path + upload_url + '/system/touchicon-orig.png',
}, { }, {
rel: 'icon', rel: 'icon',
sizes: '36x36', sizes: '36x36',
href: nconf.get('relative_path') + nconf.get('upload_url') + '/system/touchicon-36.png', href: relative_path + upload_url + '/system/touchicon-36.png',
}, { }, {
rel: 'icon', rel: 'icon',
sizes: '48x48', sizes: '48x48',
href: nconf.get('relative_path') + nconf.get('upload_url') + '/system/touchicon-48.png', href: relative_path + upload_url + '/system/touchicon-48.png',
}, { }, {
rel: 'icon', rel: 'icon',
sizes: '72x72', sizes: '72x72',
href: nconf.get('relative_path') + nconf.get('upload_url') + '/system/touchicon-72.png', href: relative_path + upload_url + '/system/touchicon-72.png',
}, { }, {
rel: 'icon', rel: 'icon',
sizes: '96x96', sizes: '96x96',
href: nconf.get('relative_path') + nconf.get('upload_url') + '/system/touchicon-96.png', href: relative_path + upload_url + '/system/touchicon-96.png',
}, { }, {
rel: 'icon', rel: 'icon',
sizes: '144x144', sizes: '144x144',
href: nconf.get('relative_path') + nconf.get('upload_url') + '/system/touchicon-144.png', href: relative_path + upload_url + '/system/touchicon-144.png',
}, { }, {
rel: 'icon', rel: 'icon',
sizes: '192x192', sizes: '192x192',
href: nconf.get('relative_path') + nconf.get('upload_url') + '/system/touchicon-192.png', href: relative_path + upload_url + '/system/touchicon-192.png',
}); });
} else { } else {
defaultLinks.push({ defaultLinks.push({
rel: 'apple-touch-icon', rel: 'apple-touch-icon',
href: nconf.get('relative_path') + '/assets/images/touch/512.png', href: relative_path + '/assets/images/touch/512.png',
}, { }, {
rel: 'icon', rel: 'icon',
sizes: '36x36', sizes: '36x36',
href: nconf.get('relative_path') + '/assets/images/touch/192.png', href: relative_path + '/assets/images/touch/192.png',
}, { }, {
rel: 'icon', rel: 'icon',
sizes: '48x48', sizes: '48x48',
href: nconf.get('relative_path') + '/assets/images/touch/144.png', href: relative_path + '/assets/images/touch/144.png',
}, { }, {
rel: 'icon', rel: 'icon',
sizes: '72x72', sizes: '72x72',
href: nconf.get('relative_path') + '/assets/images/touch/96.png', href: relative_path + '/assets/images/touch/96.png',
}, { }, {
rel: 'icon', rel: 'icon',
sizes: '96x96', sizes: '96x96',
href: nconf.get('relative_path') + '/assets/images/touch/72.png', href: relative_path + '/assets/images/touch/72.png',
}, { }, {
rel: 'icon', rel: 'icon',
sizes: '144x144', sizes: '144x144',
href: nconf.get('relative_path') + '/assets/images/touch/48.png', href: relative_path + '/assets/images/touch/48.png',
}, { }, {
rel: 'icon', rel: 'icon',
sizes: '192x192', sizes: '192x192',
href: nconf.get('relative_path') + '/assets/images/touch/36.png', href: relative_path + '/assets/images/touch/36.png',
}, { }, {
rel: 'icon', rel: 'icon',
sizes: '512x512', sizes: '512x512',
href: nconf.get('relative_path') + '/assets/images/touch/512.png', href: relative_path + '/assets/images/touch/512.png',
}); });
} }
@ -156,7 +160,7 @@ Tags.parse = async (req, data, meta, link) => {
addSiteOGImage(meta); addSiteOGImage(meta);
addIfNotExists(meta, 'property', 'og:title', Meta.config.title || 'NodeBB'); addIfNotExists(meta, 'property', 'og:title', Meta.config.title || 'NodeBB');
var ogUrl = nconf.get('url') + (req.originalUrl !== '/' ? stripRelativePath(req.originalUrl) : ''); var ogUrl = url + (req.originalUrl !== '/' ? stripRelativePath(req.originalUrl) : '');
addIfNotExists(meta, 'property', 'og:url', ogUrl); addIfNotExists(meta, 'property', 'og:url', ogUrl);
addIfNotExists(meta, 'name', 'description', Meta.config.description); addIfNotExists(meta, 'name', 'description', Meta.config.description);
addIfNotExists(meta, 'property', 'og:description', Meta.config.description); addIfNotExists(meta, 'property', 'og:description', Meta.config.description);
@ -187,8 +191,8 @@ function addIfNotExists(meta, keyName, tagName, value) {
} }
function stripRelativePath(url) { function stripRelativePath(url) {
if (url.startsWith(nconf.get('relative_path'))) { if (url.startsWith(relative_path)) {
return url.slice(nconf.get('relative_path').length); return url.slice(relative_path.length);
} }
return url; return url;
@ -198,7 +202,7 @@ function addSiteOGImage(meta) {
const key = Meta.config['og:image'] ? 'og:image' : 'brand:logo'; const key = Meta.config['og:image'] ? 'og:image' : 'brand:logo';
var ogImage = stripRelativePath(Meta.config[key] || ''); var ogImage = stripRelativePath(Meta.config[key] || '');
if (ogImage && !ogImage.startsWith('http')) { if (ogImage && !ogImage.startsWith('http')) {
ogImage = nconf.get('url') + ogImage; ogImage = url + ogImage;
} }
if (ogImage) { if (ogImage) {
@ -225,11 +229,11 @@ function addSiteOGImage(meta) {
// Push fallback logo // Push fallback logo
meta.push({ meta.push({
property: 'og:image', property: 'og:image',
content: nconf.get('url') + '/assets/images/logo@3x.png', content: url + '/assets/images/logo@3x.png',
noEscape: true, noEscape: true,
}, { }, {
property: 'og:image:url', property: 'og:image:url',
content: nconf.get('url') + '/assets/images/logo@3x.png', content: url + '/assets/images/logo@3x.png',
noEscape: true, noEscape: true,
}, { }, {
property: 'og:image:width', property: 'og:image:width',

@ -26,6 +26,8 @@ var controllers = {
const middleware = module.exports; const middleware = module.exports;
const relative_path = nconf.get('relative_path');
middleware.buildHeader = helpers.try(async function buildHeader(req, res, next) { middleware.buildHeader = helpers.try(async function buildHeader(req, res, next) {
res.locals.renderHeader = true; res.locals.renderHeader = true;
res.locals.isAPI = false; res.locals.isAPI = false;
@ -54,7 +56,7 @@ async function generateHeader(req, res, data) {
allowRegistration: registrationType === 'normal', allowRegistration: registrationType === 'normal',
searchEnabled: plugins.hasListeners('filter:search.query'), searchEnabled: plugins.hasListeners('filter:search.query'),
config: res.locals.config, config: res.locals.config,
relative_path: nconf.get('relative_path'), relative_path,
bodyClass: data.bodyClass, bodyClass: data.bodyClass,
}; };

@ -100,7 +100,6 @@ module.exports = function (middleware) {
const defaultLang = meta.config.defaultLang || 'en-GB'; const defaultLang = meta.config.defaultLang || 'en-GB';
try { try {
const codes = await languages.listCodes(); const codes = await languages.listCodes();
winston.verbose('[middleware/autoLocale] Retrieves languages list for middleware');
return _.uniq([defaultLang, ...codes]); return _.uniq([defaultLang, ...codes]);
} catch (err) { } catch (err) {
winston.error('[middleware/autoLocale] Could not retrieve languages codes list! ' + err.stack); winston.error('[middleware/autoLocale] Could not retrieve languages codes list! ' + err.stack);

@ -29,6 +29,8 @@ var delayCache = new LRU({
var middleware = module.exports; var middleware = module.exports;
const relative_path = nconf.get('relative_path');
middleware.regexes = { middleware.regexes = {
timestampedUpload: /^\d+-.+$/, timestampedUpload: /^\d+-.+$/,
}; };
@ -50,7 +52,7 @@ middleware.applyCSRF = function (req, res, next) {
}; };
middleware.applyCSRFasync = util.promisify(middleware.applyCSRF); middleware.applyCSRFasync = util.promisify(middleware.applyCSRF);
middleware.ensureLoggedIn = ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login'); middleware.ensureLoggedIn = ensureLoggedIn.ensureLoggedIn(relative_path + '/login');
Object.assign(middleware, { Object.assign(middleware, {
admin: require('./admin'), admin: require('./admin'),
@ -64,24 +66,23 @@ require('./expose')(middleware);
middleware.assert = require('./assert'); middleware.assert = require('./assert');
middleware.stripLeadingSlashes = function stripLeadingSlashes(req, res, next) { middleware.stripLeadingSlashes = function stripLeadingSlashes(req, res, next) {
var target = req.originalUrl.replace(nconf.get('relative_path'), ''); var target = req.originalUrl.replace(relative_path, '');
if (target.startsWith('//')) { if (target.startsWith('//')) {
return res.redirect(nconf.get('relative_path') + target.replace(/^\/+/, '/')); return res.redirect(relative_path + target.replace(/^\/+/, '/'));
} }
next(); next();
}; };
middleware.pageView = helpers.try(async function pageView(req, res, next) { middleware.pageView = helpers.try(async function pageView(req, res, next) {
const promises = [
analytics.pageView({ ip: req.ip, uid: req.uid }),
];
if (req.loggedIn) { if (req.loggedIn) {
promises.push(user.updateOnlineUsers(req.uid)); await Promise.all([
promises.push(user.updateLastOnlineTime(req.uid)); user.updateOnlineUsers(req.uid),
user.updateLastOnlineTime(req.uid),
]);
} }
await Promise.all(promises);
plugins.fireHook('action:middleware.pageView', { req: req });
next(); next();
await analytics.pageView({ ip: req.ip, uid: req.uid });
plugins.fireHook('action:middleware.pageView', { req: req });
}); });
middleware.pluginHooks = helpers.try(async function pluginHooks(req, res, next) { middleware.pluginHooks = helpers.try(async function pluginHooks(req, res, next) {

@ -1,6 +1,5 @@
'use strict'; 'use strict';
const util = require('util');
const nconf = require('nconf'); const nconf = require('nconf');
const validator = require('validator'); const validator = require('validator');
const winston = require('winston'); const winston = require('winston');
@ -11,6 +10,9 @@ const translator = require('../translator');
const widgets = require('../widgets'); const widgets = require('../widgets');
const utils = require('../utils'); const utils = require('../utils');
const slugify = require('../slugify'); const slugify = require('../slugify');
const cache = require('../cache');
const relative_path = nconf.get('relative_path');
module.exports = function (middleware) { module.exports = function (middleware) {
middleware.processRender = function processRender(req, res, next) { middleware.processRender = function processRender(req, res, next) {
@ -27,7 +29,7 @@ module.exports = function (middleware) {
} }
options.loggedIn = req.uid > 0; options.loggedIn = req.uid > 0;
options.relative_path = nconf.get('relative_path'); options.relative_path = relative_path;
options.template = { name: template, [template]: true }; options.template = { name: template, [template]: true };
options.url = (req.baseUrl + req.path.replace(/^\/api/, '')); options.url = (req.baseUrl + req.path.replace(/^\/api/, ''));
options.bodyClass = buildBodyClass(req, res, options); options.bodyClass = buildBodyClass(req, res, options);
@ -57,52 +59,75 @@ module.exports = function (middleware) {
req.app.set('json spaces', global.env === 'development' || req.query.pretty ? 4 : 0); req.app.set('json spaces', global.env === 'development' || req.query.pretty ? 4 : 0);
return res.json(options); return res.json(options);
} }
const ajaxifyData = JSON.stringify(options).replace(/<\//g, '<\\/');
const renderAsync = util.promisify((templateToRender, options, next) => render.call(self, templateToRender, options, next));
const results = await utils.promiseParallel({ const results = await utils.promiseParallel({
header: renderHeaderFooter('renderHeader', req, res, options), header: renderHeaderFooter('renderHeader', req, res, options),
content: renderAsync(templateToRender, options), content: renderContent(render, templateToRender, req, res, options),
footer: renderHeaderFooter('renderFooter', req, res, options), footer: renderHeaderFooter('renderFooter', req, res, options),
}); });
const str = results.header + const str = results.header +
(res.locals.postHeader || '') + (res.locals.postHeader || '') +
results.content + '<script id="ajaxify-data"></script>' + results.content +
'<script id="ajaxify-data" type="application/json">' +
JSON.stringify(options).replace(/<\//g, '<\\/') +
'</script>' +
(res.locals.preFooter || '') + (res.locals.preFooter || '') +
results.footer; results.footer;
let translated = await translate(str, req, res);
translated = translated.replace('<script id="ajaxify-data"></script>', function () {
return '<script id="ajaxify-data" type="application/json">' + ajaxifyData + '</script>';
});
if (typeof fn !== 'function') { if (typeof fn !== 'function') {
self.send(translated); self.send(str);
} else { } else {
fn(null, translated); fn(null, str);
} }
}; };
next(); next();
}; };
async function renderContent(render, tpl, req, res, options) {
return new Promise(function (resolve, reject) {
render.call(res, tpl, options, async function (err, str) {
if (err) reject(err);
else resolve(await translate(str, getLang(req, res)));
});
});
}
async function renderHeaderFooter(method, req, res, options) { async function renderHeaderFooter(method, req, res, options) {
let str = '';
const lang = getLang(req, res);
if (req.uid === 0 && res.locals.renderHeader) {
str = cache.get('render' + options.template.name + lang + method);
if (str) {
return str;
}
}
if (!str) {
if (res.locals.renderHeader) { if (res.locals.renderHeader) {
return await middleware[method](req, res, options); str = await middleware[method](req, res, options);
} else if (res.locals.renderAdminHeader) { } else if (res.locals.renderAdminHeader) {
return await middleware.admin[method](req, res, options); str = await middleware.admin[method](req, res, options);
} else {
str = '';
}
} }
return ''; const translated = await translate(str, lang);
if (req.uid === 0 && res.locals.renderHeader) {
cache.set('render' + options.template.name + lang + method, translated, 300000);
}
return translated;
} }
async function translate(str, req, res) { function getLang(req, res) {
let language = (res.locals.config && res.locals.config.userLang) || 'en-GB'; let language = (res.locals.config && res.locals.config.userLang) || 'en-GB';
if (res.locals.renderAdminHeader) { if (res.locals.renderAdminHeader) {
language = (res.locals.config && res.locals.config.acpLang) || 'en-GB'; language = (res.locals.config && res.locals.config.acpLang) || 'en-GB';
} }
language = req.query.lang ? validator.escape(String(req.query.lang)) : language; return req.query.lang ? validator.escape(String(req.query.lang)) : language;
}
async function translate(str, language) {
const translated = await translator.translate(str, language); const translated = await translator.translate(str, language);
return translator.unescape(translated); return translator.unescape(translated);
} }

@ -1,6 +1,5 @@
'use strict'; 'use strict';
const _ = require('lodash');
const validator = require('validator'); const validator = require('validator');
const plugins = require('../plugins'); const plugins = require('../plugins');
@ -54,7 +53,7 @@ function toggleEscape(navItems, flag) {
admin.get = async function () { admin.get = async function () {
if (cache) { if (cache) {
return _.cloneDeep(cache); return cache.map(item => ({ ...item }));
} }
const data = await db.getSortedSetRange('navigation:enabled', 0, -1); const data = await db.getSortedSetRange('navigation:enabled', 0, -1);
cache = data.map(function (item) { cache = data.map(function (item) {
@ -67,7 +66,7 @@ admin.get = async function () {
}); });
admin.escapeFields(cache); admin.escapeFields(cache);
return _.cloneDeep(cache); return cache.map(item => ({ ...item }));
}; };
async function getAvailable() { async function getAvailable() {

@ -6,6 +6,8 @@ const groups = require('../groups');
const navigation = module.exports; const navigation = module.exports;
const relative_path = nconf.get('relative_path');
navigation.get = async function (uid) { navigation.get = async function (uid) {
let data = await admin.get(); let data = await admin.get();
@ -13,7 +15,7 @@ navigation.get = async function (uid) {
item.originalRoute = item.route; item.originalRoute = item.route;
if (!item.route.startsWith('http')) { if (!item.route.startsWith('http')) {
item.route = nconf.get('relative_path') + item.route; item.route = relative_path + item.route;
} }
return item; return item;

@ -38,7 +38,7 @@ pagination.create = function (currentPage, pageCount, queryObj) {
return a - b; return a - b;
}); });
queryObj = _.clone(queryObj || {}); queryObj = { ...(queryObj || {}) };
delete queryObj._; delete queryObj._;

@ -87,7 +87,7 @@ module.exports = function (Plugins) {
Plugins.fireHook = async function (hook, params) { Plugins.fireHook = async function (hook, params) {
const hookList = Plugins.loadedHooks[hook]; const hookList = Plugins.loadedHooks[hook];
const hookType = hook.split(':')[0]; const hookType = hook.split(':')[0];
if (hook !== 'action:plugins.firehook') { if (global.env === 'development' && hook !== 'action:plugins.firehook') {
winston.verbose('[plugins/fireHook] ' + hook); winston.verbose('[plugins/fireHook] ' + hook);
} }

@ -44,7 +44,7 @@ module.exports = function (Posts) {
}; };
Posts.uploads.list = async function (pid) { Posts.uploads.list = async function (pid) {
return await db.getSortedSetRange('post:' + pid + ':uploads', 0, -1); return await db.getSortedSetMembers('post:' + pid + ':uploads');
}; };
Posts.uploads.listWithSizes = async function (pid) { Posts.uploads.listWithSizes = async function (pid) {

@ -95,6 +95,11 @@ function loadConfig(configFile) {
nconf.set('use_port', !!urlObject.port); nconf.set('use_port', !!urlObject.port);
nconf.set('relative_path', relativePath); nconf.set('relative_path', relativePath);
nconf.set('port', nconf.get('PORT') || nconf.get('port') || urlObject.port || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567); nconf.set('port', nconf.get('PORT') || nconf.get('port') || urlObject.port || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567);
// cookies don't provide isolation by port: http://stackoverflow.com/a/16328399/122353
const domain = nconf.get('cookieDomain') || urlObject.hostname;
const origins = nconf.get('socket.io:origins') || `${urlObject.protocol}//${domain}:*`;
nconf.set('socket.io:origins', origins);
} }
} }

@ -4,7 +4,12 @@ const helpers = module.exports;
const controllerHelpers = require('../controllers/helpers'); const controllerHelpers = require('../controllers/helpers');
helpers.setupPageRoute = function (router, name, middleware, middlewares, controller) { helpers.setupPageRoute = function (router, name, middleware, middlewares, controller) {
middlewares = [middleware.maintenanceMode, middleware.registrationComplete, middleware.pageView, middleware.pluginHooks].concat(middlewares); middlewares = [
middleware.maintenanceMode,
middleware.registrationComplete,
middleware.pageView,
middleware.pluginHooks,
].concat(middlewares);
router.get(name, middleware.busyCheck, middleware.applyCSRF, middleware.buildHeader, middlewares, helpers.tryRoute(controller)); router.get(name, middleware.busyCheck, middleware.applyCSRF, middleware.buildHeader, middlewares, helpers.tryRoute(controller));
router.get('/api' + name, middlewares, helpers.tryRoute(controller)); router.get('/api' + name, middlewares, helpers.tryRoute(controller));

@ -3,7 +3,6 @@
const os = require('os'); const os = require('os');
const nconf = require('nconf'); const nconf = require('nconf');
const winston = require('winston'); const winston = require('winston');
const url = require('url');
const util = require('util'); const util = require('util');
const cookieParser = require('cookie-parser')(nconf.get('secret')); const cookieParser = require('cookie-parser')(nconf.get('secret'));
@ -47,14 +46,7 @@ Sockets.init = function (server) {
* Can be overridden via config (socket.io:origins) * Can be overridden via config (socket.io:origins)
*/ */
if (process.env.NODE_ENV !== 'development') { if (process.env.NODE_ENV !== 'development') {
const parsedUrl = url.parse(nconf.get('url')); const origins = nconf.get('socket.io:origins');
// cookies don't provide isolation by port: http://stackoverflow.com/a/16328399/122353
const domain = nconf.get('cookieDomain') || parsedUrl.hostname;
const origins = nconf.get('socket.io:origins') || `${parsedUrl.protocol}//${domain}:*`;
nconf.set('socket.io:origins', origins);
io.origins(origins); io.origins(origins);
winston.info('[socket.io] Restricting access to origin: ' + origins); winston.info('[socket.io] Restricting access to origin: ' + origins);
} }

@ -62,7 +62,14 @@ Topics.getTopicsByTids = async function (tids, options) {
uid = options.uid; uid = options.uid;
} }
let topics = await Topics.getTopicsData(tids); const [tags, topics, hasRead, isIgnored, bookmarks, callerSettings] = await Promise.all([
Topics.getTopicsTagsObjects(tids),
Topics.getTopicsData(tids),
Topics.hasReadTopics(tids, uid),
Topics.isIgnoring(tids, uid),
Topics.getUserBookmarks(tids, uid),
user.getSettings(uid),
]);
const uids = _.uniq(topics.map(t => t && t.uid && t.uid.toString()).filter(v => utils.isNumber(v))); const uids = _.uniq(topics.map(t => t && t.uid && t.uid.toString()).filter(v => utils.isNumber(v)));
const cids = _.uniq(topics.map(t => t && t.cid && t.cid.toString()).filter(v => utils.isNumber(v))); const cids = _.uniq(topics.map(t => t && t.cid && t.cid.toString()).filter(v => utils.isNumber(v)));
@ -72,26 +79,16 @@ Topics.getTopicsByTids = async function (tids, options) {
return await Promise.all(guestTopics.map(topic => posts.getPostField(topic.mainPid, 'handle'))); return await Promise.all(guestTopics.map(topic => posts.getPostField(topic.mainPid, 'handle')));
} }
const [ const [
callerSettings,
users, users,
userSettings, userSettings,
categoriesData, categoriesData,
hasRead,
isIgnored,
bookmarks,
teasers, teasers,
tags,
guestHandles, guestHandles,
] = await Promise.all([ ] = await Promise.all([
user.getSettings(uid),
user.getUsersFields(uids, ['uid', 'username', 'fullname', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned', 'status']), user.getUsersFields(uids, ['uid', 'username', 'fullname', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned', 'status']),
user.getMultipleUserSettings(uids), user.getMultipleUserSettings(uids),
categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'backgroundImage', 'imageClass', 'bgColor', 'color', 'disabled']), categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'backgroundImage', 'imageClass', 'bgColor', 'color', 'disabled']),
Topics.hasReadTopics(tids, uid),
Topics.isIgnoring(tids, uid),
Topics.getUserBookmarks(tids, uid),
Topics.getTeasers(topics, options), Topics.getTeasers(topics, options),
Topics.getTopicsTagsObjects(tids),
loadGuestHandles(), loadGuestHandles(),
]); ]);
@ -128,9 +125,9 @@ Topics.getTopicsByTids = async function (tids, options) {
} }
}); });
topics = topics.filter(topic => topic && topic.category && !topic.category.disabled); const filteredTopics = topics.filter(topic => topic && topic.category && !topic.category.disabled);
const result = await plugins.fireHook('filter:topics.get', { topics: topics, uid: uid }); const result = await plugins.fireHook('filter:topics.get', { topics: filteredTopics, uid: uid });
return result.topics; return result.topics;
}; };

@ -56,7 +56,7 @@ module.exports = function (Topics) {
postData.forEach(function (postObj, i) { postData.forEach(function (postObj, i) {
if (postObj) { if (postObj) {
postObj.user = postObj.uid ? userData[postObj.uid] : _.clone(userData[postObj.uid]); postObj.user = postObj.uid ? userData[postObj.uid] : { ...userData[postObj.uid] };
postObj.editor = postObj.editor ? editors[postObj.editor] : null; postObj.editor = postObj.editor ? editors[postObj.editor] : null;
postObj.bookmarked = bookmarks[i]; postObj.bookmarked = bookmarks[i];
postObj.upvoted = voteData.upvotes[i]; postObj.upvoted = voteData.upvotes[i];

@ -31,7 +31,7 @@ module.exports = function (User) {
await db.delete('loginAttempts:' + uid); await db.delete('loginAttempts:' + uid);
await db.pexpire('lockout:' + uid, duration); await db.pexpire('lockout:' + uid, duration);
events.log({ await events.log({
type: 'account-locked', type: 'account-locked',
uid: uid, uid: uid,
ip: ip, ip: ip,

@ -9,6 +9,8 @@ const meta = require('../meta');
const plugins = require('../plugins'); const plugins = require('../plugins');
const utils = require('../utils'); const utils = require('../utils');
const relative_path = nconf.get('relative_path');
const intFields = [ const intFields = [
'uid', 'postcount', 'topiccount', 'reputation', 'profileviews', 'uid', 'postcount', 'topiccount', 'reputation', 'profileviews',
'banned', 'banned:expire', 'email:confirmed', 'joindate', 'lastonline', 'banned', 'banned:expire', 'email:confirmed', 'joindate', 'lastonline',
@ -104,7 +106,7 @@ module.exports = function (User) {
function uidsToUsers(uids, uniqueUids, usersData) { function uidsToUsers(uids, uniqueUids, usersData) {
const uidToUser = _.zipObject(uniqueUids, usersData); const uidToUser = _.zipObject(uniqueUids, usersData);
const users = uids.map(function (uid) { const users = uids.map(function (uid) {
const returnPayload = uidToUser[uid] || _.clone(User.guestData); const returnPayload = uidToUser[uid] || { ...User.guestData };
if (uid > 0 && !returnPayload.uid) { if (uid > 0 && !returnPayload.uid) {
returnPayload.oldUid = parseInt(uid, 10); returnPayload.oldUid = parseInt(uid, 10);
} }
@ -164,10 +166,10 @@ module.exports = function (User) {
} }
if (user.picture && user.picture === user.uploadedpicture) { if (user.picture && user.picture === user.uploadedpicture) {
user.uploadedpicture = user.picture.startsWith('http') ? user.picture : nconf.get('relative_path') + user.picture; user.uploadedpicture = user.picture.startsWith('http') ? user.picture : relative_path + user.picture;
user.picture = user.uploadedpicture; user.picture = user.uploadedpicture;
} else if (user.uploadedpicture) { } else if (user.uploadedpicture) {
user.uploadedpicture = user.uploadedpicture.startsWith('http') ? user.uploadedpicture : nconf.get('relative_path') + user.uploadedpicture; user.uploadedpicture = user.uploadedpicture.startsWith('http') ? user.uploadedpicture : relative_path + user.uploadedpicture;
} }
if (meta.config.defaultAvatar && !user.picture) { if (meta.config.defaultAvatar && !user.picture) {
user.picture = User.getDefaultAvatar(); user.picture = User.getDefaultAvatar();
@ -240,7 +242,7 @@ module.exports = function (User) {
if (!meta.config.defaultAvatar) { if (!meta.config.defaultAvatar) {
return ''; return '';
} }
return meta.config.defaultAvatar.startsWith('http') ? meta.config.defaultAvatar : nconf.get('relative_path') + meta.config.defaultAvatar; return meta.config.defaultAvatar.startsWith('http') ? meta.config.defaultAvatar : relative_path + meta.config.defaultAvatar;
}; };
User.setUserField = async function (uid, field, value) { User.setUserField = async function (uid, field, value) {

@ -50,7 +50,7 @@ module.exports.app = app;
server.on('error', function (err) { server.on('error', function (err) {
if (err.code === 'EADDRINUSE') { if (err.code === 'EADDRINUSE') {
winston.error('NodeBB address in use, exiting...', err.stack); winston.error('NodeBB address in use, exiting...\n' + err.stack);
} else { } else {
winston.error(err.stack); winston.error(err.stack);
} }

@ -3,14 +3,12 @@
const winston = require('winston'); const winston = require('winston');
const _ = require('lodash'); const _ = require('lodash');
const Benchpress = require('benchpressjs'); const Benchpress = require('benchpressjs');
const util = require('util');
const plugins = require('../plugins'); const plugins = require('../plugins');
const groups = require('../groups'); const groups = require('../groups');
const translator = require('../translator'); const translator = require('../translator');
const db = require('../database'); const db = require('../database');
const apiController = require('../controllers/api'); const apiController = require('../controllers/api');
const loadConfigAsync = util.promisify(apiController.loadConfig);
const meta = require('../meta'); const meta = require('../meta');
const widgets = module.exports; const widgets = module.exports;
@ -59,7 +57,7 @@ async function renderWidget(widget, uid, options) {
let config = options.res.locals.config || {}; let config = options.res.locals.config || {};
if (options.res.locals.isAPI) { if (options.res.locals.isAPI) {
config = await loadConfigAsync(options.req); config = await apiController.loadConfig(options.req);
} }
const userLang = config.userLang || meta.config.defaultLang || 'en-GB'; const userLang = config.userLang || meta.config.defaultLang || 'en-GB';

@ -38,6 +38,13 @@ nconf.defaults({
const urlObject = url.parse(nconf.get('url')); const urlObject = url.parse(nconf.get('url'));
const relativePath = urlObject.pathname !== '/' ? urlObject.pathname : ''; const relativePath = urlObject.pathname !== '/' ? urlObject.pathname : '';
nconf.set('relative_path', relativePath); nconf.set('relative_path', relativePath);
nconf.set('upload_path', path.join(nconf.get('base_dir'), nconf.get('upload_path')));
nconf.set('upload_url', '/assets/uploads');
// cookies don't provide isolation by port: http://stackoverflow.com/a/16328399/122353
const domain = nconf.get('cookieDomain') || urlObject.hostname;
const origins = nconf.get('socket.io:origins') || `${urlObject.protocol}//${domain}:*`;
nconf.set('socket.io:origins', origins);
if (nconf.get('isCluster') === undefined) { if (nconf.get('isCluster') === undefined) {
nconf.set('isPrimary', true); nconf.set('isPrimary', true);
@ -125,10 +132,7 @@ before(async function () {
nconf.set('base_url', urlObject.protocol + '//' + urlObject.host); nconf.set('base_url', urlObject.protocol + '//' + urlObject.host);
nconf.set('secure', urlObject.protocol === 'https:'); nconf.set('secure', urlObject.protocol === 'https:');
nconf.set('use_port', !!urlObject.port); nconf.set('use_port', !!urlObject.port);
nconf.set('relative_path', relativePath);
nconf.set('port', urlObject.port || nconf.get('port') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567); nconf.set('port', urlObject.port || nconf.get('port') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567);
nconf.set('upload_path', path.join(nconf.get('base_dir'), nconf.get('upload_path')));
nconf.set('upload_url', '/assets/uploads');
nconf.set('core_templates_path', path.join(__dirname, '../../src/views')); nconf.set('core_templates_path', path.join(__dirname, '../../src/views'));
nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates')); nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates'));

@ -1161,10 +1161,8 @@ describe('User', function () {
meta.config.defaultAvatar = 'https://path/to/default/avatar'; meta.config.defaultAvatar = 'https://path/to/default/avatar';
assert.strictEqual(User.getDefaultAvatar(), meta.config.defaultAvatar); assert.strictEqual(User.getDefaultAvatar(), meta.config.defaultAvatar);
meta.config.defaultAvatar = '/path/to/default/avatar'; meta.config.defaultAvatar = '/path/to/default/avatar';
nconf.set('relative_path', '/community'); assert.strictEqual(User.getDefaultAvatar(), nconf.get('relative_path') + meta.config.defaultAvatar);
assert.strictEqual(User.getDefaultAvatar(), '/community' + meta.config.defaultAvatar);
meta.config.defaultAvatar = ''; meta.config.defaultAvatar = '';
nconf.set('relative_path', '');
done(); done();
}); });

Loading…
Cancel
Save