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ı 4 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}
*/
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
stripHTMLTags: function (str, tags) {
var pattern = (tags || ['']).map(function (tag) {
return utils.escapeRegexChars(tag);
}).join('|');
var pattern = (tags || ['']).join('|');
return String(str).replace(new RegExp('<(\\/)?(' + (pattern || '[^\\s>]+') + ')(\\s+[^<>]*?)?\\s*(\\/)?>', 'gi'), '');
},

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

@ -157,7 +157,7 @@ Categories.getTagWhitelist = async function (cids) {
});
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');
@ -167,7 +167,7 @@ Categories.getTagWhitelist = async function (cids) {
cachedData[cid] = 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) {
@ -280,7 +280,7 @@ Categories.getTree = function (categories, parentCid) {
if (cid) {
categories[index].children = undefined;
cidToCategory[cid] = categories[index];
parents[cid] = _.clone(categories[index]);
parents[cid] = { ...categories[index] };
}
});

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

@ -15,11 +15,17 @@ const languages = require('../languages');
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) {
let config = {
relative_path: nconf.get('relative_path'),
upload_url: nconf.get('upload_url'),
assetBaseUrl: `${nconf.get('relative_path')}/assets`,
relative_path,
upload_url,
assetBaseUrl: `${relative_path}/assets`,
siteTitle: validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB')),
browserTitle: validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')),
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,
disableChatMessageEditing: meta.config.disableChatMessageEditing === 1,
maximumChatMessageLength: meta.config.maximumChatMessageLength || 1000,
socketioTransports: nconf.get('socket.io:transports') || ['polling', 'websocket'],
socketioOrigins: nconf.get('socket.io:origins'),
websocketAddress: nconf.get('socket.io:address') || '',
socketioTransports,
socketioOrigins,
websocketAddress,
maxReconnectionAttempts: meta.config.maxReconnectionAttempts || 5,
reconnectionDelay: meta.config.reconnectionDelay || 1500,
topicsPerPage: meta.config.topicsPerPage || 20,

@ -16,6 +16,9 @@ const analytics = require('../analytics');
const categoryController = module.exports;
const url = nconf.get('url');
const relative_path = nconf.get('relative_path');
categoryController.get = async function (req, res, next) {
const cid = req.params.category_id;
@ -103,7 +106,7 @@ categoryController.get = async function (req, res, next) {
categoryData.showSelect = userPrivileges.editable;
categoryData.showTopicTools = userPrivileges.editable;
categoryData.topicIndex = topicIndex;
categoryData.rssFeedUrl = nconf.get('url') + '/category/' + categoryData.cid + '.rss';
categoryData.rssFeedUrl = url + '/category/' + categoryData.cid + '.rss';
if (parseInt(req.uid, 10)) {
categories.markAsRead([cid], req.uid);
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.pagination = pagination.create(currentPage, pageCount, req.query);
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);
});
@ -128,12 +131,12 @@ async function buildBreadcrumbs(req, categoryData) {
const breadcrumbs = [
{
text: categoryData.name,
url: nconf.get('relative_path') + '/category/' + categoryData.slug,
url: relative_path + '/category/' + categoryData.slug,
cid: categoryData.cid,
},
];
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);
}
}
@ -160,7 +163,7 @@ function addTags(categoryData, res) {
if (categoryData.backgroundImage) {
if (!categoryData.backgroundImage.startsWith('http')) {
categoryData.backgroundImage = nconf.get('url') + categoryData.backgroundImage;
categoryData.backgroundImage = url + categoryData.backgroundImage;
}
res.locals.metaTags.push({
property: 'og:image',
@ -171,7 +174,7 @@ function addTags(categoryData, res) {
res.locals.linkTags = [
{
rel: 'up',
href: nconf.get('url'),
href: url,
},
];

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

@ -14,6 +14,10 @@ const analytics = require('../analytics');
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) {
const tid = req.params.topic_id;
@ -79,7 +83,7 @@ topicsController.get = async function getTopic(req, res, callback) {
topicData.scrollToMyPost = settings.scrollToMyPost;
topicData.allowMultipleBadges = meta.config.allowMultipleBadges === 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) {
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.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);
});
@ -147,7 +151,7 @@ async function buildBreadcrumbs(topicData) {
const breadcrumbs = [
{
text: topicData.category.name,
url: nconf.get('relative_path') + '/category/' + topicData.category.slug,
url: relative_path + '/category/' + topicData.category.slug,
cid: topicData.category.cid,
},
{
@ -211,7 +215,7 @@ async function addTags(topicData, req, res) {
res.locals.linkTags = [
{
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) {
res.locals.linkTags.push({
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) {
const uploads = postAtIndex ? await posts.uploads.listWithSizes(postAtIndex.pid) : [];
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;
});
if (topicData.thumb) {
@ -252,7 +256,7 @@ async function addOGImageTags(res, topicData, postAtIndex) {
function addOGImageTag(res, image) {
let imageUrl;
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') {
imageUrl = image.name;
} else {
@ -327,7 +331,7 @@ topicsController.pagination = async function (req, res, callback) {
const paginationData = pagination.create(currentPage, pageCount);
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 });

@ -4,6 +4,8 @@
const nconf = require('nconf');
const meta = require('./meta');
const relative_path = nconf.get('relative_path');
const coverPhoto = module.exports;
coverPhoto.getDefaultGroupCover = function (groupName) {
@ -15,7 +17,7 @@ coverPhoto.getDefaultProfileCover = function (uid) {
};
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']) {
const covers = String(meta.config[type + ':defaultCovers']).trim().split(/[\s,]+/g);
let coverPhoto = defaultCover;
@ -29,7 +31,7 @@ function getCover(type, id) {
id %= covers.length;
}
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;
}

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

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

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

@ -9,6 +9,10 @@ const utils = require('../utils');
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) => {
// Meta tags
const defaultTags = [{
@ -29,7 +33,7 @@ Tags.parse = async (req, data, meta, link) => {
content: Meta.config.title || 'NodeBB',
}, {
name: 'msapplication-badge',
content: 'frequency=30; polling-uri=' + nconf.get('url') + '/sitemap.xml',
content: 'frequency=30; polling-uri=' + url + '/sitemap.xml',
noEscape: true,
}, {
name: 'theme-color',
@ -55,10 +59,10 @@ Tags.parse = async (req, data, meta, link) => {
var defaultLinks = [{
rel: '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',
href: nconf.get('relative_path') + '/manifest.webmanifest',
href: relative_path + '/manifest.webmanifest',
}];
if (plugins.hasListeners('filter:search.query')) {
@ -66,7 +70,7 @@ Tags.parse = async (req, data, meta, link) => {
rel: 'search',
type: 'application/opensearchdescription+xml',
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']) {
defaultLinks.push({
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',
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',
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',
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',
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',
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',
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 {
defaultLinks.push({
rel: 'apple-touch-icon',
href: nconf.get('relative_path') + '/assets/images/touch/512.png',
href: relative_path + '/assets/images/touch/512.png',
}, {
rel: 'icon',
sizes: '36x36',
href: nconf.get('relative_path') + '/assets/images/touch/192.png',
href: relative_path + '/assets/images/touch/192.png',
}, {
rel: 'icon',
sizes: '48x48',
href: nconf.get('relative_path') + '/assets/images/touch/144.png',
href: relative_path + '/assets/images/touch/144.png',
}, {
rel: 'icon',
sizes: '72x72',
href: nconf.get('relative_path') + '/assets/images/touch/96.png',
href: relative_path + '/assets/images/touch/96.png',
}, {
rel: 'icon',
sizes: '96x96',
href: nconf.get('relative_path') + '/assets/images/touch/72.png',
href: relative_path + '/assets/images/touch/72.png',
}, {
rel: 'icon',
sizes: '144x144',
href: nconf.get('relative_path') + '/assets/images/touch/48.png',
href: relative_path + '/assets/images/touch/48.png',
}, {
rel: 'icon',
sizes: '192x192',
href: nconf.get('relative_path') + '/assets/images/touch/36.png',
href: relative_path + '/assets/images/touch/36.png',
}, {
rel: 'icon',
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);
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, 'name', '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) {
if (url.startsWith(nconf.get('relative_path'))) {
return url.slice(nconf.get('relative_path').length);
if (url.startsWith(relative_path)) {
return url.slice(relative_path.length);
}
return url;
@ -198,7 +202,7 @@ function addSiteOGImage(meta) {
const key = Meta.config['og:image'] ? 'og:image' : 'brand:logo';
var ogImage = stripRelativePath(Meta.config[key] || '');
if (ogImage && !ogImage.startsWith('http')) {
ogImage = nconf.get('url') + ogImage;
ogImage = url + ogImage;
}
if (ogImage) {
@ -225,11 +229,11 @@ function addSiteOGImage(meta) {
// Push fallback logo
meta.push({
property: 'og:image',
content: nconf.get('url') + '/assets/images/logo@3x.png',
content: url + '/assets/images/logo@3x.png',
noEscape: true,
}, {
property: 'og:image:url',
content: nconf.get('url') + '/assets/images/logo@3x.png',
content: url + '/assets/images/logo@3x.png',
noEscape: true,
}, {
property: 'og:image:width',

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

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

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

@ -1,6 +1,5 @@
'use strict';
const util = require('util');
const nconf = require('nconf');
const validator = require('validator');
const winston = require('winston');
@ -11,6 +10,9 @@ const translator = require('../translator');
const widgets = require('../widgets');
const utils = require('../utils');
const slugify = require('../slugify');
const cache = require('../cache');
const relative_path = nconf.get('relative_path');
module.exports = function (middleware) {
middleware.processRender = function processRender(req, res, next) {
@ -27,7 +29,7 @@ module.exports = function (middleware) {
}
options.loggedIn = req.uid > 0;
options.relative_path = nconf.get('relative_path');
options.relative_path = relative_path;
options.template = { name: template, [template]: true };
options.url = (req.baseUrl + req.path.replace(/^\/api/, ''));
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);
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({
header: renderHeaderFooter('renderHeader', req, res, options),
content: renderAsync(templateToRender, options),
content: renderContent(render, templateToRender, req, res, options),
footer: renderHeaderFooter('renderFooter', req, res, options),
});
const str = results.header +
(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 || '') +
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') {
self.send(translated);
self.send(str);
} else {
fn(null, translated);
fn(null, str);
}
};
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) {
if (res.locals.renderHeader) {
return await middleware[method](req, res, options);
} else if (res.locals.renderAdminHeader) {
return await middleware.admin[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) {
str = await middleware[method](req, res, options);
} else if (res.locals.renderAdminHeader) {
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';
if (res.locals.renderAdminHeader) {
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);
return translator.unescape(translated);
}

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

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

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

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

@ -44,7 +44,7 @@ module.exports = function (Posts) {
};
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) {

@ -95,6 +95,11 @@ function loadConfig(configFile) {
nconf.set('use_port', !!urlObject.port);
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);
// 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');
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('/api' + name, middlewares, helpers.tryRoute(controller));

@ -3,7 +3,6 @@
const os = require('os');
const nconf = require('nconf');
const winston = require('winston');
const url = require('url');
const util = require('util');
const cookieParser = require('cookie-parser')(nconf.get('secret'));
@ -47,14 +46,7 @@ Sockets.init = function (server) {
* Can be overridden via config (socket.io:origins)
*/
if (process.env.NODE_ENV !== 'development') {
const parsedUrl = url.parse(nconf.get('url'));
// 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);
const origins = nconf.get('socket.io:origins');
io.origins(origins);
winston.info('[socket.io] Restricting access to origin: ' + origins);
}

@ -62,7 +62,14 @@ Topics.getTopicsByTids = async function (tids, options) {
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 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')));
}
const [
callerSettings,
users,
userSettings,
categoriesData,
hasRead,
isIgnored,
bookmarks,
teasers,
tags,
guestHandles,
] = await Promise.all([
user.getSettings(uid),
user.getUsersFields(uids, ['uid', 'username', 'fullname', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned', 'status']),
user.getMultipleUserSettings(uids),
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.getTopicsTagsObjects(tids),
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;
};

@ -56,7 +56,7 @@ module.exports = function (Topics) {
postData.forEach(function (postObj, i) {
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.bookmarked = bookmarks[i];
postObj.upvoted = voteData.upvotes[i];

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

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

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

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

@ -38,6 +38,13 @@ nconf.defaults({
const urlObject = url.parse(nconf.get('url'));
const relativePath = urlObject.pathname !== '/' ? urlObject.pathname : '';
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) {
nconf.set('isPrimary', true);
@ -125,10 +132,7 @@ before(async function () {
nconf.set('base_url', urlObject.protocol + '//' + urlObject.host);
nconf.set('secure', urlObject.protocol === 'https:');
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('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('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';
assert.strictEqual(User.getDefaultAvatar(), meta.config.defaultAvatar);
meta.config.defaultAvatar = '/path/to/default/avatar';
nconf.set('relative_path', '/community');
assert.strictEqual(User.getDefaultAvatar(), '/community' + meta.config.defaultAvatar);
assert.strictEqual(User.getDefaultAvatar(), nconf.get('relative_path') + meta.config.defaultAvatar);
meta.config.defaultAvatar = '';
nconf.set('relative_path', '');
done();
});

Loading…
Cancel
Save