Merge remote-tracking branch 'origin/master'

# Conflicts:
#	package.json
This commit is contained in:
Barış Soner Uşaklı
2017-10-05 11:40:58 -04:00
43 changed files with 375 additions and 260 deletions

View File

@@ -89,7 +89,7 @@ uploadsController.uploadLogo = function (req, res, next) {
uploadsController.uploadSound = function (req, res, next) {
var uploadedFile = req.files.files[0];
var mimeType = mime.lookup(uploadedFile.name);
var mimeType = mime.getType(uploadedFile.name);
if (!/^audio\//.test(mimeType)) {
return next(Error('[[error:invalid-data]]'));
}

View File

@@ -3,6 +3,7 @@
var nconf = require('nconf');
var winston = require('winston');
var validator = require('validator');
var plugins = require('../plugins');
exports.handleURIErrors = function (err, req, res, next) {
// Handle cases where malformed URIs are passed in
@@ -35,30 +36,50 @@ exports.handleURIErrors = function (err, req, res, next) {
// this needs to have four arguments or express treats it as `(req, res, next)`
// don't remove `next`!
exports.handleErrors = function (err, req, res, next) { // eslint-disable-line no-unused-vars
switch (err.code) {
case 'EBADCSRFTOKEN':
winston.error(req.path + '\n', err.message);
return res.sendStatus(403);
case 'blacklisted-ip':
return res.status(403).type('text/plain').send(err.message);
}
var cases = {
EBADCSRFTOKEN: function () {
winston.error(req.path + '\n', err.message);
res.sendStatus(403);
},
'blacklisted-ip': function () {
res.status(403).type('text/plain').send(err.message);
},
};
var defaultHandler = function () {
// Display NodeBB error page
var status = parseInt(err.status, 10);
if ((status === 302 || status === 308) && err.path) {
return res.locals.isAPI ? res.set('X-Redirect', err.path).status(200).json(err.path) : res.redirect(err.path);
}
var status = parseInt(err.status, 10);
if ((status === 302 || status === 308) && err.path) {
return res.locals.isAPI ? res.set('X-Redirect', err.path).status(200).json(err.path) : res.redirect(err.path);
}
winston.error(req.path + '\n', err.stack);
winston.error(req.path + '\n', err.stack);
res.status(status || 500);
res.status(status || 500);
var path = String(req.path || '');
if (res.locals.isAPI) {
res.json({ path: validator.escape(path), error: err.message });
} else {
var middleware = require('../middleware');
middleware.buildHeader(req, res, function () {
res.render('500', { path: validator.escape(path), error: validator.escape(String(err.message)) });
});
}
};
var path = String(req.path || '');
if (res.locals.isAPI) {
res.json({ path: validator.escape(path), error: err.message });
} else {
var middleware = require('../middleware');
middleware.buildHeader(req, res, function () {
res.render('500', { path: validator.escape(path), error: validator.escape(String(err.message)) });
});
}
plugins.fireHook('filter:error.handle', {
cases: cases,
}, function (_err, data) {
if (_err) {
// Assume defaults
winston.warn('[errors/handle] Unable to retrieve plugin handlers for errors: ' + _err.message);
data.cases = cases;
}
if (data.cases.hasOwnProperty(err.code)) {
data.cases[err.code](err, req, res, defaultHandler);
} else {
defaultHandler();
}
});
};

View File

@@ -8,7 +8,7 @@ var meta = require('../meta');
module.exports.handle = function (req, res, next) {
if (plugins.hasListeners('filter:search.query')) {
res.type('application/xml').send(generateXML());
res.type('application/opensearchdescription+xml').send(generateXML());
} else {
next();
}
@@ -17,9 +17,21 @@ module.exports.handle = function (req, res, next) {
function generateXML() {
return xml([{
OpenSearchDescription: [
{ _attr: { xmlns: 'http://a9.com/-/spec/opensearch/1.1/' } },
{ ShortName: String(meta.config.title || meta.config.browserTitle || 'NodeBB') },
{ Description: String(meta.config.description || '') },
{ _attr: {
xmlns: 'http://a9.com/-/spec/opensearch/1.1/',
'xmlns:moz': 'http://www.mozilla.org/2006/browser/search/',
} },
{ ShortName: trimToLength(String(meta.config.title || meta.config.browserTitle || 'NodeBB'), 16) },
{ Description: trimToLength(String(meta.config.description || ''), 1024) },
{ InputEncoding: 'UTF-8' },
{ Image: [
{ _attr: {
width: '16',
height: '16',
type: 'image/x-icon',
} },
nconf.get('url') + '/favicon.ico',
] },
{ Url: {
_attr: {
type: 'text/html',
@@ -27,6 +39,11 @@ function generateXML() {
template: nconf.get('url') + '/search?term={searchTerms}&in=titlesposts',
},
} },
{ 'moz:SearchForm': nconf.get('url') + '/search' },
],
}], { declaration: true, indent: '\t' });
}
function trimToLength(string, length) {
return string.trim().substring(0, length).trim();
}

View File

@@ -56,17 +56,33 @@ Emailer.registerApp = function (expressApp) {
// Enable Gmail transport if enabled in ACP
if (parseInt(meta.config['email:smtpTransport:enabled'], 10) === 1) {
var smtpOptions = {
auth: {
var smtpOptions = {};
if (meta.config['email:smtpTransport:user'] || meta.config['email:smtpTransport:pass']) {
smtpOptions.auth = {
user: meta.config['email:smtpTransport:user'],
pass: meta.config['email:smtpTransport:pass'],
},
};
};
}
if (meta.config['email:smtpTransport:serice'] === 'nodebb-custom-smtp') {
if (meta.config['email:smtpTransport:service'] === 'nodebb-custom-smtp') {
smtpOptions.port = meta.config['email:smtpTransport:port'];
smtpOptions.host = meta.config['email:smtpTransport:host'];
smtpOptions.secure = true;
if (meta.config['email:smtpTransport:security'] === 'NONE') {
smtpOptions.secure = false;
smtpOptions.requireTLS = false;
smtpOptions.ignoreTLS = true;
} else if (meta.config['email:smtpTransport:security'] === 'STARTTLS') {
smtpOptions.secure = false;
smtpOptions.requireTLS = true;
smtpOptions.ignoreTLS = false;
} else {
// meta.config['email:smtpTransport:security'] === 'ENCRYPTED' or undefined
smtpOptions.secure = true;
smtpOptions.requireTLS = true;
smtpOptions.ignoreTLS = false;
}
} else {
smtpOptions.service = meta.config['email:smtpTransport:service'];
}

View File

@@ -45,7 +45,7 @@ file.saveFileToLocal = function (filename, folder, tempPath, callback) {
};
file.base64ToLocal = function (imageData, uploadPath, callback) {
var buffer = new Buffer(imageData.slice(imageData.indexOf('base64') + 7), 'base64');
var buffer = Buffer.from(imageData.slice(imageData.indexOf('base64') + 7), 'base64');
uploadPath = path.join(nconf.get('upload_path'), uploadPath);
fs.writeFile(uploadPath, buffer, {
@@ -141,7 +141,7 @@ file.linkDirs = function linkDirs(sourceDir, destDir, callback) {
file.typeToExtension = function (type) {
var extension;
if (type) {
extension = '.' + mime.extension(type);
extension = '.' + mime.getExtension(type);
}
return extension;
};

View File

@@ -240,7 +240,7 @@ Flags.validate = function (payload, callback) {
}
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
// Check if reporter meets rep threshold (or can edit the target post, in which case threshold does not apply)
// Check if reporter meets rep threshold (or can edit the target post, in which case threshold does not apply)
if (!editable.flag && parseInt(data.reporter.reputation, 10) < minimumReputation) {
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
}
@@ -256,7 +256,7 @@ Flags.validate = function (payload, callback) {
}
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
// Check if reporter meets rep threshold (or can edit the target user, in which case threshold does not apply)
// Check if reporter meets rep threshold (or can edit the target user, in which case threshold does not apply)
if (!editable && parseInt(data.reporter.reputation, 10) < minimumReputation) {
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
}

View File

@@ -26,7 +26,7 @@ module.exports = function (Groups) {
var tempPath = data.file ? data.file : '';
var url;
var type = data.file ? mime.lookup(data.file) : 'image/png';
var type = data.file ? mime.getType(data.file) : 'image/png';
async.waterfall([
function (next) {

View File

@@ -141,7 +141,7 @@ image.writeImageDataToTempFile = function (imageData, callback) {
var filepath = path.join(os.tmpdir(), filename + extension);
var buffer = new Buffer(imageData.slice(imageData.indexOf('base64') + 7), 'base64');
var buffer = Buffer.from(imageData.slice(imageData.indexOf('base64') + 7), 'base64');
fs.writeFile(filepath, buffer, {
encoding: 'base64',

View File

@@ -20,7 +20,7 @@ Blacklist.load = function (callback) {
Blacklist.get,
Blacklist.validate,
function (rules, next) {
winston.verbose('[meta/blacklist] Loading ' + rules.valid.length + ' blacklist rules');
winston.verbose('[meta/blacklist] Loading ' + rules.valid.length + ' blacklist rule(s)' + (rules.duplicateCount > 0 ? ', ignored ' + rules.duplicateCount + ' duplicate(s)' : ''));
if (rules.invalid.length) {
winston.warn('[meta/blacklist] ' + rules.invalid.length + ' invalid blacklist rule(s) were ignored.');
}
@@ -44,8 +44,8 @@ Blacklist.save = function (rules, callback) {
db.setObject('ip-blacklist-rules', { rules: rules }, next);
},
function (next) {
Blacklist.load(next);
pubsub.publish('blacklist:reload');
setImmediate(next);
},
], callback);
};
@@ -71,8 +71,11 @@ Blacklist.test = function (clientIp, callback) {
Blacklist._rules.ipv4.indexOf(clientIp) === -1 && // not explicitly specified in ipv4 list
Blacklist._rules.ipv6.indexOf(clientIp) === -1 && // not explicitly specified in ipv6 list
!Blacklist._rules.cidr.some(function (subnet) {
return addr.match(ipaddr.parseCIDR(subnet));
// return ip.cidrSubnet(subnet).contains(clientIp);
var cidr = ipaddr.parseCIDR(subnet);
if (addr.kind() !== cidr[0].kind()) {
return false;
}
return addr.match(cidr);
}) // not in a blacklisted IPv4 or IPv6 cidr range
) {
plugins.fireHook('filter:blacklist.test', { // To return test failure, pass back an error in callback
@@ -108,6 +111,7 @@ Blacklist.validate = function (rules, callback) {
var ipv6 = [];
var cidr = [];
var invalid = [];
var duplicateCount = 0;
var inlineCommentMatch = /#.*$/;
var whitelist = ['127.0.0.1', '::1', '::ffff:0:127.0.0.1'];
@@ -119,6 +123,16 @@ Blacklist.validate = function (rules, callback) {
return rule.length && !rule.startsWith('#') ? rule : null;
}).filter(Boolean);
// Filter out duplicates
rules = rules.filter(function (rule, index) {
const pass = rules.indexOf(rule) === index;
if (!pass) {
duplicateCount += 1;
}
return pass;
});
// Filter out invalid rules
rules = rules.filter(function (rule) {
var addr;
@@ -164,6 +178,7 @@ Blacklist.validate = function (rules, callback) {
cidr: cidr,
valid: rules,
invalid: invalid,
duplicateCount: duplicateCount,
});
};

View File

@@ -282,9 +282,9 @@ function buildCSS(data, callback) {
processImportFrom: ['local'],
}),
] : [autoprefixer]).process(lessOutput.css).then(function (result) {
callback(null, { code: result.css });
process.nextTick(callback, null, { code: result.css });
}, function (err) {
callback(err);
process.nextTick(callback, err);
});
});
}

View File

@@ -66,6 +66,7 @@ Tags.parse = function (req, data, meta, link, callback) {
defaultLinks.push({
rel: 'search',
type: 'application/opensearchdescription+xml',
title: validator.escape(String(Meta.config.title || Meta.config.browserTitle || 'NodeBB')),
href: nconf.get('relative_path') + '/osd.xml',
});
}

View File

@@ -9,6 +9,7 @@ var nconf = require('nconf');
var ensureLoggedIn = require('connect-ensure-login');
var toobusy = require('toobusy-js');
var Benchpress = require('benchpressjs');
var LRU = require('lru-cache');
var plugins = require('../plugins');
var meta = require('../meta');
@@ -23,6 +24,10 @@ var controllers = {
helpers: require('../controllers/helpers'),
};
var delayCache = LRU({
maxAge: 1000 * 60,
});
var middleware = module.exports;
middleware.applyCSRF = csrf();
@@ -186,6 +191,14 @@ middleware.processTimeagoLocales = function (req, res, next) {
middleware.delayLoading = function (req, res, next) {
// Introduces an artificial delay during load so that brute force attacks are effectively mitigated
// Add IP to cache so if too many requests are made, subsequent requests are blocked for a minute
var timesSeen = delayCache.get(req.ip) || 0;
if (timesSeen > 10) {
return res.sendStatus(429);
}
delayCache.set(req.ip, timesSeen += 1);
setTimeout(next, 1000);
};

View File

@@ -39,8 +39,7 @@ module.exports = function (Plugins) {
(Plugins.deprecatedHooks[data.hook] ?
'please use `' + Plugins.deprecatedHooks[data.hook] + '` instead.' :
'there is no alternative.'
)
);
));
} else {
// handle hook's startsWith, i.e. action:homepage.get
var parts = data.hook.split(':');
@@ -61,7 +60,7 @@ module.exports = function (Plugins) {
if (memo && memo[prop]) {
return memo[prop];
}
// Couldn't find method by path, aborting
// Couldn't find method by path, aborting
return null;
}, Plugins.libraries[data.id]);

View File

@@ -35,7 +35,7 @@ module.exports = function (Topics) {
var extension = path.extname(data.thumb);
if (!extension) {
extension = '.' + mime.extension(type);
extension = '.' + mime.getExtension(type);
}
filename = Date.now() + '-topic-thumb' + extension;
pathToUpload = path.join(nconf.get('upload_path'), 'files', filename);

View File

@@ -26,7 +26,7 @@ module.exports = {
}
fs.access(sourcePath, function (err) {
if (err) {
if (err || path.extname(sourcePath) === '.svg') {
skip = true;
return setImmediate(next);
}
@@ -46,6 +46,7 @@ module.exports = {
meta.configs.setMultiple({
'brand:logo': path.join(nconf.get('upload_path'), 'system', path.basename(meta.config['brand:logo'])),
'brand:emailLogo': '/assets/uploads/system/site-logo-x50.png',
}, next);
},
], callback);

View File

@@ -28,7 +28,7 @@ module.exports = function (User) {
var uploadSize = parseInt(meta.config.maximumProfileImageSize, 10) || 256;
var size = res.headers['content-length'];
var type = res.headers['content-type'];
var extension = mime.extension(type);
var extension = mime.getExtension(type);
if (['png', 'jpeg', 'jpg', 'gif'].indexOf(extension) === -1) {
return callback(new Error('[[error:invalid-image-extension]]'));

View File

@@ -1,12 +1,14 @@
<script>
define('/assets/templates/500.js', function () {
function compiled(helpers, context, get, iter, helper) {
return '<div class="alert alert-danger">\n\t<strong>[[global:500.title]]</strong>\n\t<p>[[global:500.message]]</p>\n\t<p>' +
helpers.__escape(get(context && context['path'])) + '</p>\n\t' +
(get(context && context['error']) ? '<p>' + helpers.__escape(get(context && context['error'])) + '</p>' : '') + '\n\n\t' +
(get(context && context['returnLink']) ? '\n\t<p>[[error:goback]]</p>\n\t' : '') + '\n</div>\n';
}
window.addEventListener('load', function () {
define('/assets/templates/500.js', function () {
function compiled(helpers, context, get, iter, helper) {
return '<div class="alert alert-danger">\n\t<strong>[[global:500.title]]</strong>\n\t<p>[[global:500.message]]</p>\n\t<p>' +
helpers.__escape(get(context && context['path'])) + '</p>\n\t' +
(get(context && context['error']) ? '<p>' + helpers.__escape(get(context && context['error'])) + '</p>' : '') + '\n\n\t' +
(get(context && context['returnLink']) ? '\n\t<p>[[error:goback]]</p>\n\t' : '') + '\n</div>\n';
}
return compiled;
});
return compiled;
});
});
</script>

View File

@@ -40,12 +40,12 @@
<div class="form-group">
<label for="email:smtpTransport:service"><strong>[[admin/settings/email:smtp-transport.service]]</strong></label>
<select class="form-control input-lg" id="email:smtpTransport:service" data-field="email:smtpTransport:service">
<option value="nodebb-custom-smtp" style="font-weight: bold">[[admin/settings/email:smtp-transport.service-custom]]</option>
<option style="font-size: 10px" disabled>&nbsp;</option>
<!-- BEGIN services -->
<option value="@value">@value</option>
<!-- END services -->
<option style="font-size: 10px" disabled>&nbsp;</option>
<option value="nodebb-custom-smtp" style="font-weight: bold">[[admin/settings/email:smtp-transport.service-custom]]</option>
</select>
<p class="help-block">
[[admin/settings/email:smtp-transport.service-help]]
@@ -63,6 +63,13 @@
<label for="email:smtpTransport:port">[[admin/settings/email:smtp-transport.port]]</label>
<input type="text" class="form-control input-md" id="email:smtpTransport:port" data-field="email:smtpTransport:port" placeholder="5555">
<label for="email:smtpTransport:security">[[admin/settings/email:smtp-transport.security]]</label>
<select class="form-control" id="email:smtpTransport:security" data-field="email:smtpTransport:security">
<option value="ENCRYPTED">[[admin/settings/email:smtp-transport.security-encrypted]]</option>
<option value="STARTTLS">[[admin/settings/email:smtp-transport.security-starttls]]</option>
<option value="NONE">[[admin/settings/email:smtp-transport.security-none]]</option>
</select>
</div>
<div class="form-group">
<label for="email:smtpTransport:user"><strong>[[admin/settings/email:smtp-transport.username]]</strong></label>
@@ -136,4 +143,4 @@
</div>
</div>
<!-- IMPORT admin/partials/settings/footer.tpl -->
<!-- IMPORT admin/partials/settings/footer.tpl -->

View File

@@ -180,8 +180,15 @@ function setupExpressApp(app, callback) {
setupAutoLocale(app, callback);
}
function ping(req, res) {
res.status(200).send(req.path === '/sping' ? 'healthy' : '200');
function ping(req, res, next) {
async.waterfall([
function (next) {
db.getObject('config', next);
},
function () {
res.status(200).send(req.path === '/sping' ? 'healthy' : '200');
},
], next);
}
function setupFavicon(app) {