Merge commit 'bc41848adb6c2a84c74a88598b87334a179ecabf' into v1.11.x

This commit is contained in:
Misty (Bot)
2019-01-24 21:28:57 +00:00
381 changed files with 2793 additions and 1396 deletions

View File

@@ -26,7 +26,7 @@ var uniquevisitors = 0;
* the cache could be exhausted continuously if there are more than 500 concurrently
* active users
*/
var ipCache = LRU({
var ipCache = new LRU({
max: 500,
length: function () { return 1; },
maxAge: 0,

View File

@@ -3,7 +3,7 @@
var LRU = require('lru-cache');
var pubsub = require('./pubsub');
var cache = LRU({
var cache = new LRU({
max: 1000,
maxAge: 0,
});

View File

@@ -52,7 +52,7 @@ module.exports = function (Categories) {
'cid:' + cid + ':tids:posts',
'cid:' + cid + ':pids',
'cid:' + cid + ':read_by_uid',
'cid:' + cid + ':ignorers',
'cid:' + cid + ':uid:watch:state',
'cid:' + cid + ':children',
'cid:' + cid + ':tag:whitelist',
'category:' + cid,

View File

@@ -6,7 +6,7 @@ var _ = require('lodash');
var db = require('../database');
var user = require('../user');
var Groups = require('../groups');
var groups = require('../groups');
var plugins = require('../plugins');
var privileges = require('../privileges');
const cache = require('../cache');
@@ -21,6 +21,7 @@ require('./unread')(Categories);
require('./activeusers')(Categories);
require('./recentreplies')(Categories);
require('./update')(Categories);
require('./watch')(Categories);
Categories.exists = function (cid, callback) {
db.exists('category:' + cid, callback);
@@ -45,8 +46,8 @@ Categories.getCategoryById = function (data, callback) {
topicCount: function (next) {
Categories.getTopicCount(data, next);
},
isIgnored: function (next) {
Categories.isIgnored([data.cid], data.uid, next);
watchState: function (next) {
Categories.getWatchState([data.cid], data.uid, next);
},
parent: function (next) {
if (category.parentCid) {
@@ -64,7 +65,9 @@ Categories.getCategoryById = function (data, callback) {
category.topics = results.topics.topics;
category.nextStart = results.topics.nextStart;
category.topic_count = results.topicCount;
category.isIgnored = results.isIgnored[0];
category.isWatched = results.watchState[0] === Categories.watchStates.watching;
category.isNotWatched = results.watchState[0] === Categories.watchStates.notwatching;
category.isIgnored = results.watchState[0] === Categories.watchStates.ignoring;
category.parent = results.parent;
calculateTopicPostCount(category);
@@ -76,14 +79,6 @@ Categories.getCategoryById = function (data, callback) {
], callback);
};
Categories.isIgnored = function (cids, uid, callback) {
if (parseInt(uid, 10) <= 0) {
return setImmediate(callback, null, cids.map(() => false));
}
const keys = cids.map(cid => 'cid:' + cid + ':ignorers');
db.isMemberOfSortedSets(keys, uid, callback);
};
Categories.getAllCidsFromSet = function (key, callback) {
const cids = cache.get(key);
if (cids) {
@@ -135,10 +130,45 @@ Categories.getCategoriesByPrivilege = function (set, uid, privilege, callback) {
Categories.getModerators = function (cid, callback) {
async.waterfall([
function (next) {
Groups.getMembers('cid:' + cid + ':privileges:moderate', 0, -1, next);
Categories.getModeratorUids([cid], next);
},
function (uids, next) {
user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture'], next);
user.getUsersFields(uids[0], ['uid', 'username', 'userslug', 'picture'], next);
},
], callback);
};
Categories.getModeratorUids = function (cids, callback) {
var sets;
async.waterfall([
function (next) {
var groupNames = cids.reduce(function (memo, cid) {
memo.push('cid:' + cid + ':privileges:moderate');
memo.push('cid:' + cid + ':privileges:groups:moderate');
return memo;
}, []);
groups.getMembersOfGroups(groupNames, next);
},
function (memberSets, next) {
// Every other set is actually a list of user groups, not uids, so convert those to members
sets = memberSets.reduce(function (memo, set, idx) {
if (idx % 2) {
memo.groupNames.push(set);
} else {
memo.uids.push(set);
}
return memo;
}, { groupNames: [], uids: [] });
groups.getMembersOfGroups(sets.groupNames, next);
},
function (groupUids, next) {
const moderatorUids = cids.map(function (cid, index) {
return _.union(sets.uids[index].concat(groupUids[index]));
});
next(null, moderatorUids);
},
], callback);
};
@@ -443,20 +473,4 @@ Categories.buildForSelectCategories = function (categories, callback) {
callback(null, categoriesData);
};
Categories.getIgnorers = function (cid, start, stop, callback) {
db.getSortedSetRevRange('cid:' + cid + ':ignorers', start, stop, callback);
};
Categories.filterIgnoringUids = function (cid, uids, callback) {
async.waterfall([
function (next) {
db.isSortedSetMembers('cid:' + cid + ':ignorers', uids, next);
},
function (isIgnoring, next) {
const readingUids = uids.filter((uid, index) => uid && !isIgnoring[index]);
next(null, readingUids);
},
], callback);
};
Categories.async = require('../promisify')(Categories);

80
src/categories/watch.js Normal file
View File

@@ -0,0 +1,80 @@
'use strict';
const async = require('async');
const db = require('../database');
const user = require('../user');
module.exports = function (Categories) {
Categories.watchStates = {
ignoring: 1,
notwatching: 2,
watching: 3,
};
Categories.isIgnored = function (cids, uid, callback) {
if (!(parseInt(uid, 10) > 0)) {
return setImmediate(callback, null, cids.map(() => false));
}
async.waterfall([
function (next) {
Categories.getWatchState(cids, uid, next);
},
function (states, next) {
next(null, states.map(state => state === Categories.watchStates.ignoring));
},
], callback);
};
Categories.getWatchState = function (cids, uid, callback) {
if (!(parseInt(uid, 10) > 0)) {
return setImmediate(callback, null, cids.map(() => Categories.watchStates.notwatching));
}
if (!Array.isArray(cids) || !cids.length) {
return setImmediate(callback, null, []);
}
async.waterfall([
function (next) {
const keys = cids.map(cid => 'cid:' + cid + ':uid:watch:state');
async.parallel({
userSettings: async.apply(user.getSettings, uid),
states: async.apply(db.sortedSetsScore, keys, uid),
}, next);
},
function (results, next) {
next(null, results.states.map(state => state || Categories.watchStates[results.userSettings.categoryWatchState]));
},
], callback);
};
Categories.getIgnorers = function (cid, start, stop, callback) {
const count = (stop === -1) ? -1 : (stop - start + 1);
db.getSortedSetRevRangeByScore('cid:' + cid + ':uid:watch:state', start, count, Categories.watchStates.ignoring, Categories.watchStates.ignoring, callback);
};
Categories.filterIgnoringUids = function (cid, uids, callback) {
async.waterfall([
function (next) {
Categories.getUidsWatchStates(cid, uids, next);
},
function (states, next) {
const readingUids = uids.filter((uid, index) => uid && states[index] !== Categories.watchStates.ignoring);
next(null, readingUids);
},
], callback);
};
Categories.getUidsWatchStates = function (cid, uids, callback) {
async.waterfall([
function (next) {
async.parallel({
userSettings: async.apply(user.getMultipleUserSettings, uids),
states: async.apply(db.sortedSetScores, 'cid:' + cid + ':uid:watch:state', uids),
}, next);
},
function (results, next) {
next(null, results.states.map((state, index) => state || Categories.watchStates[results.userSettings[index].categoryWatchState]));
},
], callback);
};
};

View File

@@ -3,6 +3,8 @@
var fs = require('fs');
var path = require('path');
require('../../require-main');
var packageInstall = require('./package-install');
var dirname = require('./paths').baseDir;

View File

@@ -7,9 +7,9 @@ var validator = require('validator');
var meta = require('../meta');
var plugins = require('../plugins');
exports.handle404 = function (req, res) {
exports.handle404 = function handle404(req, res) {
var relativePath = nconf.get('relative_path');
var isClientScript = new RegExp('^' + relativePath + '\\/assets\\/src\\/.+\\.js');
var isClientScript = new RegExp('^' + relativePath + '\\/assets\\/src\\/.+\\.js(\\?v=\\w+)?$');
if (plugins.hasListeners('action:meta.override404')) {
return plugins.fireHook('action:meta.override404', {

View File

@@ -21,8 +21,8 @@ categoriesController.get = function (req, res, callback) {
}
async.parallel({
ignored: function (next) {
user.getIgnoredCategories(userData.uid, next);
states: function (next) {
user.getCategoryWatchState(userData.uid, next);
},
categories: function (next) {
categories.buildForSelect(userData.uid, 'find', next);
@@ -32,7 +32,9 @@ categoriesController.get = function (req, res, callback) {
function (results) {
results.categories.forEach(function (category) {
if (category) {
category.isIgnored = results.ignored.includes(String(category.cid));
category.isIgnored = results.states[category.cid] === categories.watchStates.ignoring;
category.isWatched = results.states[category.cid] === categories.watchStates.watching;
category.isNotWatched = results.states[category.cid] === categories.watchStates.notwatching;
}
});
userData.categories = results.categories;

View File

@@ -41,11 +41,13 @@ profileController.get = function (req, res, callback) {
}
userData = _userData;
req.session.uids_viewed = req.session.uids_viewed || {};
if (req.uid >= 0) {
req.session.uids_viewed = req.session.uids_viewed || {};
if (req.uid !== userData.uid && (!req.session.uids_viewed[userData.uid] || req.session.uids_viewed[userData.uid] < Date.now() - 3600000)) {
user.incrementUserFieldBy(userData.uid, 'profileviews', 1);
req.session.uids_viewed[userData.uid] = Date.now();
if (req.uid !== userData.uid && (!req.session.uids_viewed[userData.uid] || req.session.uids_viewed[userData.uid] < Date.now() - 3600000)) {
user.incrementUserFieldBy(userData.uid, 'profileviews', 1);
req.session.uids_viewed[userData.uid] = Date.now();
}
}
async.parallel({

View File

@@ -1,7 +1,10 @@
'use strict';
var async = require('async');
var nconf = require('nconf');
var winston = require('winston');
var _ = require('lodash');
var jwt = require('jsonwebtoken');
var user = require('../../user');
var languages = require('../../languages');
@@ -161,6 +164,8 @@ settingsController.get = function (req, res, callback) {
};
});
userData.categoryWatchState = { [userData.settings.categoryWatchState]: true };
userData.disableCustomUserSkins = meta.config.disableCustomUserSkins;
userData.allowUserHomePage = meta.config.allowUserHomePage;
@@ -181,6 +186,52 @@ settingsController.get = function (req, res, callback) {
], callback);
};
settingsController.unsubscribe = function (req, res) {
if (!req.params.token) {
return res.sendStatus(404);
}
jwt.verify(req.params.token, nconf.get('secret'), function (err, payload) {
if (err) {
return res.sendStatus(403);
}
switch (payload.template) {
case 'digest':
async.parallel([
async.apply(user.setSetting, payload.uid, 'dailyDigestFreq', 'off'),
async.apply(user.updateDigestSetting, payload.uid, 'off'),
], function (err) {
if (err) {
winston.error('[settings/unsubscribe] One-click unsubscribe failed with error: ' + err.message);
return res.sendStatus(500);
}
return res.sendStatus(200);
});
break;
case 'notification':
async.waterfall([
async.apply(db.getObjectField, 'user:' + payload.uid + ':settings', 'notificationType_' + payload.type),
(current, next) => {
user.setSetting(payload.uid, 'notificationType_' + payload.type, (current === 'notificationemail' ? 'notification' : 'none'), next);
},
], function (err) {
if (err) {
winston.error('[settings/unsubscribe] One-click unsubscribe failed with error: ' + err.message);
return res.sendStatus(500);
}
return res.sendStatus(200);
});
break;
default:
res.sendStatus(404);
break;
}
});
};
function getNotificationSettings(userData, callback) {
var privilegedTypes = [];

View File

@@ -1,5 +1,6 @@
'use strict';
const validator = require('validator');
var plugins = require('../../plugins');
var hooksController = module.exports;
@@ -18,7 +19,7 @@ hooksController.get = function (req, res) {
current.methods.push({
id: hookData.id,
priority: hookData.priority,
method: hookData.method ? hookData.method.toString() : 'No plugin function!',
method: hookData.method ? validator.escape(hookData.method.toString()) : 'No plugin function!',
index: hookIndex + '-code-' + methodIndex,
});
});

View File

@@ -12,6 +12,7 @@ var categories = require('../categories');
var privileges = require('../privileges');
var plugins = require('../plugins');
var translator = require('../translator');
var languages = require('../languages');
var apiController = module.exports;
@@ -57,11 +58,12 @@ apiController.loadConfig = function (req, callback) {
config.requireEmailConfirmation = meta.config.requireEmailConfirmation === 1;
config.topicPostSort = meta.config.topicPostSort || 'oldest_to_newest';
config.categoryTopicSort = meta.config.categoryTopicSort || 'newest_to_oldest';
config.csrf_token = req.csrfToken && req.csrfToken();
config.csrf_token = req.uid >= 0 && req.csrfToken && req.csrfToken();
config.searchEnabled = plugins.hasListeners('filter:search.query');
config.bootswatchSkin = meta.config.bootswatchSkin || '';
config.enablePostHistory = (meta.config.enablePostHistory || 1) === 1;
config.notificationAlertTimeout = meta.config.notificationAlertTimeout || 5000;
config.timeagoCodes = languages.timeagoCodes;
if (config.useOutgoingLinksPage) {
config.outgoingLinksWhitelist = meta.config['outgoingLinks:whitelist'];
@@ -101,6 +103,10 @@ apiController.loadConfig = function (req, callback) {
config.bootswatchSkin = (meta.config.disableCustomUserSkins !== 1 && settings.bootswatchSkin && settings.bootswatchSkin !== '') ? settings.bootswatchSkin : '';
plugins.fireHook('filter:config.get', config, next);
},
function (config, next) {
req.res.locals.config = config;
process.nextTick(next, null, config);
},
], callback);
};

View File

@@ -19,7 +19,6 @@ var privileges = require('../privileges');
var sockets = require('../socket.io');
var authenticationController = module.exports;
var apiController = require('./api');
authenticationController.register = function (req, res) {
var registrationType = meta.config.registrationType || 'normal';
@@ -286,10 +285,10 @@ function continueLogin(req, res, next) {
} else {
delete req.query.lang;
async.parallel({
async.series({
doLogin: async.apply(authenticationController.doLogin, req, userData.uid),
buildHeader: async.apply(middleware.buildHeader, req, res),
header: async.apply(middleware.generateHeader, req, res, {}),
config: async.apply(apiController.loadConfig, req),
}, function (err, payload) {
if (err) {
return helpers.noScriptErrors(req, res, err.message, 403);
@@ -309,7 +308,7 @@ function continueLogin(req, res, next) {
res.status(200).send({
next: destination,
header: payload.header,
config: payload.config,
config: res.locals.config,
});
}
});
@@ -474,33 +473,40 @@ authenticationController.logout = function (req, res, next) {
req.logout();
req.session.regenerate(function (err) {
req.uid = 0;
req.headers['x-csrf-token'] = req.csrfToken();
next(err);
});
},
function (next) {
user.setUserField(req.uid, 'lastonline', Date.now() - 300000, next);
user.setUserField(req.uid, 'lastonline', Date.now() - (meta.config.onlineCutoff * 60000), next);
},
function (next) {
db.sortedSetRemove('users:online', req.uid, next);
},
function (next) {
plugins.fireHook('static:user.loggedOut', { req: req, res: res, uid: req.uid }, next);
},
async.apply(middleware.autoLocale, req, res),
function () {
// Force session check for all connected socket.io clients with the same session id
sockets.in('sess_' + req.sessionID).emit('checkSession', 0);
if (req.body.noscript === 'true') {
res.redirect(nconf.get('relative_path') + '/');
} else {
async.parallel({
async.series({
buildHeader: async.apply(middleware.buildHeader, req, res),
header: async.apply(middleware.generateHeader, req, res, {}),
config: async.apply(apiController.loadConfig, req),
}, function (err, payload) {
if (err) {
return res.status(500);
}
res.status(200).send({
payload = {
header: payload.header,
config: payload.config,
});
config: res.locals.config,
};
plugins.fireHook('filter:user.logout', payload);
res.status(200).send(payload);
});
}
},

View File

@@ -5,7 +5,7 @@ var winston = require('winston');
var validator = require('validator');
var plugins = require('../plugins');
exports.handleURIErrors = function (err, req, res, next) {
exports.handleURIErrors = function handleURIErrors(err, req, res, next) {
// Handle cases where malformed URIs are passed in
if (err instanceof URIError) {
const cleanPath = req.path.replace(new RegExp('^' + nconf.get('relative_path')), '');
@@ -36,7 +36,7 @@ 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
exports.handleErrors = function handleErrors(err, req, res, next) { // eslint-disable-line no-unused-vars
var cases = {
EBADCSRFTOKEN: function () {
winston.error(req.path + '\n', err.message);

View File

@@ -239,6 +239,20 @@ helpers.getCategories = function (set, uid, privilege, selectedCid, callback) {
], callback);
};
helpers.getCategoriesByStates = function (uid, selectedCid, states, callback) {
async.waterfall([
function (next) {
user.getCategoriesByStates(uid, states, next);
},
function (cids, next) {
privileges.categories.filterCids('read', cids, uid, next);
},
function (cids, next) {
getCategoryData(cids, uid, selectedCid, next);
},
], callback);
};
helpers.getWatchedCategories = function (uid, selectedCid, callback) {
async.waterfall([
function (next) {

View File

@@ -92,7 +92,7 @@ Controllers.login = function (req, res, next) {
var registrationType = meta.config.registrationType || 'normal';
var allowLoginWith = (meta.config.allowLoginWith || 'username-email');
var returnTo = (req.headers['x-return-to'] || '').replace(nconf.get('base_url'), '');
var returnTo = (req.headers['x-return-to'] || '').replace(nconf.get('base_url') + nconf.get('relative_path'), '');
var errorText;
if (req.query.error === 'csrf-invalid') {
@@ -214,7 +214,7 @@ Controllers.registerInterstitial = function (req, res, next) {
// No interstitials, redirect to home
const returnTo = req.session.returnTo || req.session.registration.returnTo;
delete req.session.registration;
return helpers.redirect(res, returnTo || nconf.get('relative_path') + '/');
return helpers.redirect(res, returnTo || '/');
}
var renders = data.interstitials.map(function (interstitial) {
return async.apply(req.app.render.bind(req.app), interstitial.template, interstitial.data || {});
@@ -252,6 +252,7 @@ Controllers.robots = function (req, res) {
res.send('User-agent: *\n' +
'Disallow: ' + nconf.get('relative_path') + '/admin/\n' +
'Disallow: ' + nconf.get('relative_path') + '/reset/\n' +
'Disallow: ' + nconf.get('relative_path') + '/compose\n' +
'Sitemap: ' + nconf.get('url') + '/sitemap.xml');
}
};

View File

@@ -5,6 +5,7 @@ var async = require('async');
var nconf = require('nconf');
var user = require('../user');
var categories = require('../categories');
var topics = require('../topics');
var meta = require('../meta');
var helpers = require('./helpers');
@@ -47,8 +48,8 @@ recentController.getData = function (req, url, sort, callback) {
settings: function (next) {
user.getSettings(req.uid, next);
},
watchedCategories: function (next) {
helpers.getWatchedCategories(req.uid, cid, next);
categories: function (next) {
helpers.getCategoriesByStates(req.uid, cid, [categories.watchStates.watching, categories.watchStates.notwatching], next);
},
rssToken: function (next) {
user.auth.getFeedToken(req.uid, next);
@@ -58,7 +59,7 @@ recentController.getData = function (req, url, sort, callback) {
function (results, next) {
rssToken = results.rssToken;
settings = results.settings;
categoryData = results.watchedCategories;
categoryData = results.categories;
var start = Math.max(0, (page - 1) * settings.topicsPerPage);
stop = start + settings.topicsPerPage - 1;

View File

@@ -3,6 +3,7 @@
var async = require('async');
var nconf = require('nconf');
var winston = require('winston');
var user = require('../user');
var meta = require('../meta');
@@ -17,7 +18,7 @@ var analytics = require('../analytics');
var topicsController = module.exports;
topicsController.get = function (req, res, callback) {
topicsController.get = function getTopic(req, res, callback) {
var tid = req.params.topic_id;
var currentPage = parseInt(req.query.page, 10) || 1;
var pageCount = 1;
@@ -161,16 +162,18 @@ topicsController.get = function (req, res, callback) {
res.locals.linkTags.push(rel);
});
req.session.tids_viewed = req.session.tids_viewed || {};
if (!req.session.tids_viewed[tid] || req.session.tids_viewed[tid] < Date.now() - 3600000) {
topics.increaseViewCount(tid);
req.session.tids_viewed[tid] = Date.now();
if (req.uid >= 0) {
req.session.tids_viewed = req.session.tids_viewed || {};
if (!req.session.tids_viewed[tid] || req.session.tids_viewed[tid] < Date.now() - 3600000) {
topics.increaseViewCount(tid);
req.session.tids_viewed[tid] = Date.now();
}
}
if (req.loggedIn) {
topics.markAsRead([tid], req.uid, function (err, markedRead) {
if (err) {
return callback(err);
return winston.error(err);
}
if (markedRead) {
topics.pushUnreadCount(req.uid);

View File

@@ -8,6 +8,7 @@ var querystring = require('querystring');
var meta = require('../meta');
var pagination = require('../pagination');
var user = require('../user');
var categories = require('../categories');
var topics = require('../topics');
var plugins = require('../plugins');
var helpers = require('./helpers');
@@ -35,7 +36,7 @@ unreadController.get = function (req, res, next) {
if (plugins.hasListeners('filter:unread.categories')) {
plugins.fireHook('filter:unread.categories', { uid: req.uid, cid: cid }, next);
} else {
helpers.getWatchedCategories(req.uid, cid, next);
helpers.getCategoriesByStates(req.uid, cid, [categories.watchStates.watching], next);
}
},
settings: function (next) {

View File

@@ -168,31 +168,6 @@ uploadsController.uploadThumb = function (req, res, next) {
}, next);
};
uploadsController.uploadGroupCover = function (uid, uploadedFile, callback) {
if (plugins.hasListeners('filter:uploadImage')) {
return plugins.fireHook('filter:uploadImage', {
image: uploadedFile,
uid: uid,
}, callback);
}
if (plugins.hasListeners('filter:uploadFile')) {
return plugins.fireHook('filter:uploadFile', {
file: uploadedFile,
uid: uid,
}, callback);
}
async.waterfall([
function (next) {
file.isFileTypeAllowed(uploadedFile.path, next);
},
function (next) {
saveFileToLocal(uid, uploadedFile, next);
},
], callback);
};
uploadsController.uploadFile = function (uid, uploadedFile, callback) {
if (plugins.hasListeners('filter:uploadFile')) {
return plugins.fireHook('filter:uploadFile', {

View File

@@ -15,17 +15,24 @@ coverPhoto.getDefaultProfileCover = function (uid) {
};
function getCover(type, id) {
const defaultCover = nconf.get('relative_path') + '/assets/images/cover-default.png';
if (meta.config[type + ':defaultCovers']) {
var covers = meta.config[type + ':defaultCovers'].trim().split(/[\s,]+/g);
var covers = String(meta.config[type + ':defaultCovers']).trim().split(/[\s,]+/g);
let coverPhoto = defaultCover;
if (!covers.length) {
return coverPhoto;
}
if (typeof id === 'string') {
id = (id.charCodeAt(0) + id.charCodeAt(1)) % covers.length;
} else {
id %= covers.length;
}
return covers[id];
if (covers[id] && !covers[id].startsWith('http')) {
coverPhoto = nconf.get('relative_path') + covers[id];
}
return coverPhoto;
}
return nconf.get('relative_path') + '/assets/images/cover-default.png';
return defaultCover;
}

View File

@@ -4,7 +4,7 @@ module.exports.create = function (name) {
var LRU = require('lru-cache');
var pubsub = require('../pubsub');
var cache = LRU({
var cache = new LRU({
max: 20000,
length: function () { return 1; },
maxAge: 0,

View File

@@ -16,10 +16,9 @@ module.exports = function (db, module) {
if (!key || !data) {
return callback();
}
if (data.hasOwnProperty('')) {
delete data[''];
}
db.collection('objects').updateOne({ _key: key }, { $set: data }, { upsert: true, w: 1 }, function (err) {
const writeData = helpers.serializeData(data);
db.collection('objects').updateOne({ _key: key }, { $set: writeData }, { upsert: true, w: 1 }, function (err) {
if (err) {
return callback(err);
}
@@ -34,7 +33,6 @@ module.exports = function (db, module) {
return callback();
}
var data = {};
field = helpers.fieldToString(field);
data[field] = value;
module.setObject(key, data, callback);
};
@@ -76,7 +74,7 @@ module.exports = function (db, module) {
if (err) {
return callback(err);
}
data = data.map(helpers.deserializeData);
var map = helpers.toMap(data);
unCachedKeys.forEach(function (key) {
cachedData[key] = map[key] || null;

View File

@@ -22,8 +22,27 @@ helpers.fieldToString = function (field) {
field = field.toString();
}
// if there is a '.' in the field name it inserts subdocument in mongo, replace '.'s with \uff0E
field = field.replace(/\./g, '\uff0E');
return field;
return field.replace(/\./g, '\uff0E');
};
helpers.serializeData = function (data) {
const serialized = {};
for (const field in data) {
if (data.hasOwnProperty(field) && field !== '') {
serialized[helpers.fieldToString(field)] = data[field];
}
}
return serialized;
};
helpers.deserializeData = function (data) {
const deserialized = {};
for (const field in data) {
if (data.hasOwnProperty(field)) {
deserialized[field.replace(/\uff0E/g, '.')] = data[field];
}
}
return deserialized;
};
helpers.valueToString = function (value) {

View File

@@ -55,6 +55,9 @@ module.exports = function (db, module) {
}
bulk.execute(function (err) {
if (err && err.message.startsWith('E11000 duplicate key error')) {
return process.nextTick(module.setsAdd, keys, value, callback);
}
callback(err);
});
};

View File

@@ -56,6 +56,10 @@ module.exports = function (db, module) {
query.score.$lte = max;
}
if (max === min) {
query.score = max;
}
const fields = { _id: 0, _key: 0 };
if (!withScores) {
fields.score = 0;
@@ -115,10 +119,10 @@ module.exports = function (db, module) {
};
function getSortedSetRangeByScore(key, start, count, min, max, sort, withScores, callback) {
if (parseInt(count, 10) === -1) {
count = 0;
if (parseInt(count, 10) === 0) {
return setImmediate(callback, null, []);
}
var stop = start + count - 1;
const stop = (parseInt(count, 10) === -1) ? -1 : (start + count - 1);
getSortedSetRange(key, start, stop, min, max, sort, withScores, callback);
}
@@ -261,7 +265,7 @@ module.exports = function (db, module) {
module.sortedSetsScore = function (keys, value, callback) {
if (!Array.isArray(keys) || !keys.length) {
return callback();
return callback(null, []);
}
value = helpers.valueToString(value);
db.collection('objects').find({ _key: { $in: keys }, value: value }, { projection: { _id: 0, value: 0 } }).toArray(function (err, result) {
@@ -269,22 +273,27 @@ module.exports = function (db, module) {
return callback(err);
}
var map = helpers.toMap(result);
var returnData = [];
var item;
var map = {};
result.forEach(function (item) {
if (item) {
map[item._key] = item;
}
});
for (var i = 0; i < keys.length; i += 1) {
item = map[keys[i]];
returnData.push(item ? item.score : null);
}
result = keys.map(function (key) {
return map[key] ? map[key].score : null;
});
callback(null, returnData);
callback(null, result);
});
};
module.sortedSetScores = function (key, values, callback) {
if (!key) {
return callback(null, null);
return setImmediate(callback, null, null);
}
if (!values.length) {
return setImmediate(callback, null, []);
}
values = values.map(helpers.valueToString);
db.collection('objects').find({ _key: key, value: { $in: values } }, { projection: { _id: 0, _key: 0 } }).toArray(function (err, result) {

View File

@@ -1,6 +1,7 @@
'use strict';
var async = require('async');
var _ = require('lodash');
module.exports = function (db, module) {
var helpers = module.helpers.postgres;
@@ -44,9 +45,7 @@ SELECT $1::TEXT, m
value = [value];
}
keys = keys.filter(function (k, i, a) {
return a.indexOf(k) === i;
});
keys = _.uniq(keys);
module.transaction(function (tx, done) {
var query = tx.client.query.bind(tx.client);

View File

@@ -348,7 +348,7 @@ SELECT z."score" s
module.sortedSetsScore = function (keys, value, callback) {
if (!Array.isArray(keys) || !keys.length) {
return callback();
return callback(null, []);
}
value = helpers.valueToString(value);
@@ -382,9 +382,11 @@ SELECT o."_key" k,
module.sortedSetScores = function (key, values, callback) {
if (!key) {
return callback(null, null);
return setImmediate(callback, null, null);
}
if (!values.length) {
return setImmediate(callback, null, []);
}
values = values.map(helpers.valueToString);
query({

View File

@@ -170,6 +170,9 @@ module.exports = function (redisClient, module) {
};
module.sortedSetsScore = function (keys, value, callback) {
if (!Array.isArray(keys) || !keys.length) {
return callback(null, []);
}
helpers.execKeysValue(redisClient, 'batch', 'zscore', keys, value, function (err, scores) {
if (err) {
return callback(err);
@@ -182,6 +185,9 @@ module.exports = function (redisClient, module) {
};
module.sortedSetScores = function (key, values, callback) {
if (!values.length) {
return setImmediate(callback, null, []);
}
helpers.execKeyValues(redisClient, 'batch', 'zscore', key, values, function (err, scores) {
if (err) {
return callback(err);

View File

@@ -11,6 +11,7 @@ var url = require('url');
var path = require('path');
var fs = require('fs');
var _ = require('lodash');
var jwt = require('jsonwebtoken');
var User = require('./user');
var Plugins = require('./plugins');
@@ -110,7 +111,7 @@ Emailer.setupFallbackTransport = function (config) {
smtpOptions.ignoreTLS = false;
}
} else {
smtpOptions.service = config['email:smtpTransport:service'];
smtpOptions.service = String(config['email:smtpTransport:service']);
}
Emailer.transports.smtp = nodemailer.createTransport(smtpOptions);
@@ -216,6 +217,31 @@ Emailer.sendToEmail = function (template, email, language, params, callback) {
'List-Unsubscribe': '<' + [nconf.get('url'), 'uid', params.uid, 'settings'].join('/') + '>',
}, params.headers);
// Digests and notifications can be one-click unsubbed
let payload = {
template: template,
uid: params.uid,
};
switch (template) {
case 'digest':
payload = jwt.sign(payload, nconf.get('secret'), {
expiresIn: '30d',
});
params.headers['List-Unsubscribe'] = '<' + [nconf.get('url'), 'email', 'unsubscribe', payload].join('/') + '>';
params.headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click';
break;
case 'notification':
payload.type = params.notification.type;
payload = jwt.sign(payload, nconf.get('secret'), {
expiresIn: '30d',
});
params.headers['List-Unsubscribe'] = '<' + [nconf.get('url'), 'email', 'unsubscribe', payload].join('/') + '>';
params.headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click';
break;
}
async.waterfall([
function (next) {
Plugins.fireHook('filter:email.params', {

View File

@@ -158,9 +158,7 @@ events.deleteEvents = function (eids, callback) {
var keys;
async.waterfall([
function (next) {
keys = eids.map(function (eid) {
return 'event:' + eid;
});
keys = eids.map(eid => 'event:' + eid);
db.getObjectsFields(keys, ['type'], next);
},
function (eventData, next) {

View File

@@ -3,7 +3,7 @@
var LRU = require('lru-cache');
var pubsub = require('../pubsub');
var cache = LRU({
var cache = new LRU({
max: 40000,
maxAge: 0,
});

View File

@@ -2,12 +2,10 @@
var async = require('async');
var path = require('path');
var mime = require('mime');
var db = require('../database');
var image = require('../image');
var file = require('../file');
var uploadsController = require('../controllers/uploads');
module.exports = function (Groups) {
Groups.updateCoverPosition = function (groupName, position, callback) {
@@ -25,7 +23,7 @@ module.exports = function (Groups) {
var tempPath = data.file ? data.file : '';
var url;
var type = data.file ? mime.getType(data.file) : 'image/png';
async.waterfall([
function (next) {
if (tempPath) {
@@ -36,10 +34,10 @@ module.exports = function (Groups) {
function (_tempPath, next) {
tempPath = _tempPath;
uploadsController.uploadGroupCover(uid, {
name: 'groupCover' + path.extname(tempPath),
const filename = 'groupCover-' + data.groupName + path.extname(tempPath);
image.uploadImage(filename, 'files', {
path: tempPath,
type: type,
uid: uid,
}, next);
},
function (uploadData, next) {
@@ -53,10 +51,9 @@ module.exports = function (Groups) {
}, next);
},
function (next) {
uploadsController.uploadGroupCover(uid, {
name: 'groupCoverThumb' + path.extname(tempPath),
image.uploadImage('groupCoverThumb-' + data.groupName + path.extname(tempPath), 'files', {
path: tempPath,
type: type,
uid: uid,
}, next);
},
function (uploadData, next) {

View File

@@ -2,6 +2,7 @@
var async = require('async');
var validator = require('validator');
var nconf = require('nconf');
var db = require('../database');
var plugins = require('../plugins');
@@ -91,8 +92,20 @@ function modifyGroup(group, fields) {
group.createtimeISO = utils.toISOString(group.createtime);
group.private = ([null, undefined].includes(group.private)) ? 1 : group.private;
group['cover:url'] = group['cover:url'] || require('../coverPhoto').getDefaultGroupCover(group.name);
group['cover:thumb:url'] = group['cover:thumb:url'] || group['cover:url'];
if (group['cover:url']) {
group['cover:url'] = group['cover:url'].startsWith('http') ? group['cover:url'] : (nconf.get('relative_path') + group['cover:url']);
} else {
group['cover:url'] = require('../coverPhoto').getDefaultGroupCover(group.name);
}
if (group['cover:thumb:url']) {
group['cover:thumb:url'] = group['cover:thumb:url'].startsWith('http') ? group['cover:thumb:url'] : (nconf.get('relative_path') + group['cover:thumb:url']);
} else {
group['cover:thumb:url'] = require('../coverPhoto').getDefaultGroupCover(group.name);
}
group['cover:position'] = validator.escape(String(group['cover:position'] || '50% 50%'));
}
}

View File

@@ -105,7 +105,7 @@ Groups.getGroupsAndMembers = function (groupNames, callback) {
data.groups.forEach(function (group, index) {
if (group) {
group.members = data.members[index] || [];
group.truncated = group.memberCount > data.members.length;
group.truncated = group.memberCount > group.members.length;
}
});
next(null, data.groups);

View File

@@ -20,6 +20,7 @@ module.exports = function (Groups) {
async.parallel({
notification: function (next) {
notifications.create({
type: 'group-request-membership',
bodyShort: '[[groups:request.notification_title, ' + username + ']]',
bodyLong: '[[groups:request.notification_text, ' + username + ', ' + groupName + ']]',
nid: 'group:' + groupName + ':uid:' + uid + ':request',
@@ -133,9 +134,7 @@ module.exports = function (Groups) {
};
Groups.getMembersOfGroups = function (groupNames, callback) {
db.getSortedSetsMembers(groupNames.map(function (name) {
return 'group:' + name + ':members';
}), callback);
db.getSortedSetsMembers(groupNames.map(name => 'group:' + name + ':members'), callback);
};
Groups.isMember = function (uid, groupName, callback) {

View File

@@ -15,20 +15,23 @@ module.exports = function (Groups) {
async.waterfall([
async.apply(db.getSortedSetRange, 'groups:createtime', 0, -1),
function (groupNames, next) {
// Ephemeral groups and the registered-users groups are searchable
groupNames = Groups.ephemeralGroups.concat(groupNames);
if (!options.hideEphemeralGroups) {
groupNames = Groups.ephemeralGroups.concat(groupNames);
}
groupNames = groupNames.filter(function (name) {
return name.toLowerCase().includes(query) && name !== 'administrators' && !Groups.isPrivilegeGroup(name);
});
groupNames = groupNames.slice(0, 100);
Groups.getGroupsData(groupNames, next);
if (options.showMembers) {
Groups.getGroupsAndMembers(groupNames, next);
} else {
Groups.getGroupsData(groupNames, next);
}
},
function (groupsData, next) {
groupsData = groupsData.filter(Boolean);
if (options.filterHidden) {
groupsData = groupsData.filter(function (group) {
return !group.hidden;
});
groupsData = groupsData.filter(group => !group.hidden);
}
Groups.sort(options.sort, groupsData, next);

View File

@@ -139,3 +139,28 @@ image.writeImageDataToTempFile = function (imageData, callback) {
image.sizeFromBase64 = function (imageData) {
return Buffer.from(imageData.slice(imageData.indexOf('base64') + 7), 'base64').length;
};
image.uploadImage = function (filename, folder, image, callback) {
if (plugins.hasListeners('filter:uploadImage')) {
return plugins.fireHook('filter:uploadImage', {
image: image,
uid: image.uid,
}, callback);
}
async.waterfall([
function (next) {
file.isFileTypeAllowed(image.path, next);
},
function (next) {
file.saveFileToLocal(filename, folder, image.path, next);
},
function (upload, next) {
next(null, {
url: upload.url,
path: upload.path,
name: image.name,
});
},
], callback);
};

View File

@@ -7,6 +7,9 @@ var async = require('async');
var Languages = module.exports;
var languagesPath = path.join(__dirname, '../build/public/language');
const files = fs.readdirSync(path.join(__dirname, '../public/vendor/jquery/timeago/locales'));
Languages.timeagoCodes = files.filter(f => f.startsWith('jquery.timeago')).map(f => f.split('.')[2]);
Languages.get = function (language, namespace, callback) {
fs.readFile(path.join(languagesPath, language, namespace + '.json'), { encoding: 'utf-8' }, function (err, data) {
if (err) {

View File

@@ -167,19 +167,14 @@ function build(targets, options, callback) {
return aliases[target];
})
// filter nonexistent targets
.filter(Boolean)
// map multitargets to their sets
.reduce(function (prev, target) {
if (Array.isArray(targetHandlers[target])) {
return prev.concat(targetHandlers[target]);
}
.filter(Boolean);
return prev.concat(target);
}, [])
// unique
.filter(function (target, i, arr) {
return arr.indexOf(target) === i;
});
// map multitargets to their sets
targets = _.uniq(_.flatMap(targets, target => (
Array.isArray(targetHandlers[target]) ?
targetHandlers[target] :
target
)));
winston.verbose('[build] building the following targets: ' + targets.join(', '));

View File

@@ -232,7 +232,7 @@ function minifyAndSave(data, callback) {
var minified = uglify.minify(scripts, {
sourceMap: {
filename: data.filename,
url: data.filename + '.map',
url: String(data.filename).split(/[/\\]/).pop() + '.map',
includeSources: true,
},
compress: false,

View File

@@ -4,6 +4,7 @@ var path = require('path');
var async = require('async');
var nconf = require('nconf');
var jsesc = require('jsesc');
var _ = require('lodash');
var db = require('../database');
var user = require('../user');
@@ -24,12 +25,16 @@ var controllers = {
};
module.exports = function (middleware) {
middleware.buildHeader = function (req, res, next) {
middleware.buildHeader = function buildHeader(req, res, next) {
res.locals.renderHeader = true;
res.locals.isAPI = false;
async.waterfall([
function (next) {
middleware.applyCSRF(req, res, next);
if (req.uid >= 0) {
middleware.applyCSRF(req, res, next);
} else {
setImmediate(next);
}
},
function (next) {
async.parallel({
@@ -42,13 +47,13 @@ module.exports = function (middleware) {
}, next);
},
function (results, next) {
res.locals.config = results.config;
next();
// Return no arguments
setImmediate(next);
},
], next);
};
middleware.generateHeader = function (req, res, data, callback) {
middleware.generateHeader = function generateHeader(req, res, data, callback) {
var registrationType = meta.config.registrationType || 'normal';
res.locals.config = res.locals.config || {};
var templateValues = {
@@ -108,7 +113,7 @@ module.exports = function (middleware) {
banned: async.apply(user.isBanned, req.uid),
banReason: async.apply(user.getBannedReason, req.uid),
unreadCounts: async.apply(topics.getUnreadTids, { uid: req.uid, count: true }),
unreadData: async.apply(topics.getUnreadData, { uid: req.uid }),
unreadChatCount: async.apply(messaging.getUnreadCount, req.uid),
unreadNotificationCount: async.apply(user.notifications.getUnreadCount, req.uid),
}, next);
@@ -119,6 +124,14 @@ module.exports = function (middleware) {
return res.redirect('/');
}
const unreadData = {
'': {},
new: {},
watched: {},
unreplied: {},
};
results.user.unreadData = unreadData;
results.user.isAdmin = results.isAdmin;
results.user.isGlobalMod = results.isGlobalMod;
results.user.isMod = !!results.isModerator;
@@ -129,13 +142,15 @@ module.exports = function (middleware) {
results.user['email:confirmed'] = results.user['email:confirmed'] === 1;
results.user.isEmailConfirmSent = !!results.isEmailConfirmSent;
templateValues.bootswatchSkin = parseInt(meta.config.disableCustomUserSkins, 10) !== 1 ? res.locals.config.bootswatchSkin || '' : '';
templateValues.bootswatchSkin = (parseInt(meta.config.disableCustomUserSkins, 10) !== 1 ? res.locals.config.bootswatchSkin : '') || meta.config.bootswatchSkin || '';
templateValues.config.bootswatchSkin = templateValues.bootswatchSkin || 'noskin'; // TODO remove in v1.12.0+
const unreadCounts = results.unreadData.counts;
var unreadCount = {
topic: results.unreadCounts[''] || 0,
newTopic: results.unreadCounts.new || 0,
watchedTopic: results.unreadCounts.watched || 0,
unrepliedTopic: results.unreadCounts.unreplied || 0,
topic: unreadCounts[''] || 0,
newTopic: unreadCounts.new || 0,
watchedTopic: unreadCounts.watched || 0,
unrepliedTopic: unreadCounts.unreplied || 0,
chat: results.unreadChatCount || 0,
notification: results.unreadNotificationCount || 0,
};
@@ -146,19 +161,21 @@ module.exports = function (middleware) {
}
});
const tidsByFilter = results.unreadData.tidsByFilter;
results.navigation = results.navigation.map(function (item) {
function modifyNavItem(item, route, count, content) {
function modifyNavItem(item, route, filter, content) {
if (item && item.originalRoute === route) {
unreadData[filter] = _.zipObject(tidsByFilter[filter], tidsByFilter[filter].map(() => true));
item.content = content;
if (count > 0) {
if (unreadCounts[filter] > 0) {
item.iconClass += ' unread-count';
}
}
}
modifyNavItem(item, '/unread', results.unreadCounts[''], unreadCount.topic);
modifyNavItem(item, '/unread?filter=new', results.unreadCounts.new, unreadCount.newTopic);
modifyNavItem(item, '/unread?filter=watched', results.unreadCounts.watched, unreadCount.watchedTopic);
modifyNavItem(item, '/unread?filter=unreplied', results.unreadCounts.unreplied, unreadCount.unrepliedTopic);
modifyNavItem(item, '/unread', '', unreadCount.topic);
modifyNavItem(item, '/unread?filter=new', 'new', unreadCount.newTopic);
modifyNavItem(item, '/unread?filter=watched', 'watched', unreadCount.watchedTopic);
modifyNavItem(item, '/unread?filter=unreplied', 'unreplied', unreadCount.unrepliedTopic);
return item;
});
@@ -202,7 +219,7 @@ module.exports = function (middleware) {
});
};
middleware.renderHeader = function (req, res, data, callback) {
middleware.renderHeader = function renderHeader(req, res, data, callback) {
async.waterfall([
async.apply(middleware.generateHeader, req, res, data),
function (templateValues, next) {
@@ -211,7 +228,7 @@ module.exports = function (middleware) {
], callback);
};
middleware.renderFooter = function (req, res, data, callback) {
middleware.renderFooter = function renderFooter(req, res, data, callback) {
async.waterfall([
function (next) {
plugins.fireHook('filter:middleware.renderFooter', {
@@ -224,20 +241,18 @@ module.exports = function (middleware) {
async.parallel({
scripts: async.apply(plugins.fireHook, 'filter:scripts.get', []),
timeagoLocale: (next) => {
const userLang = res.locals.config.userLang;
const pathToLocaleFile = '/vendor/jquery/timeago/locales/jquery.timeago.' + utils.userLangToTimeagoCode(userLang) + '.js';
async.waterfall([
async.apply(languages.list),
(languages, next) => {
if (!languages.some(obj => obj.code === userLang)) {
return next(null, false);
}
async.apply(languages.listCodes),
(languageCodes, next) => {
const userLang = res.locals.config.userLang;
const timeagoCode = utils.userLangToTimeagoCode(userLang);
file.exists(path.join(__dirname, '../../public', pathToLocaleFile), next);
},
(exists, next) => {
next(null, exists ? (nconf.get('relative_path') + '/assets' + pathToLocaleFile) : null);
if (languageCodes.includes(userLang) && languages.timeagoCodes.includes(timeagoCode)) {
const pathToLocaleFile = '/vendor/jquery/timeago/locales/jquery.timeago.' + timeagoCode + '.js';
next(null, (nconf.get('relative_path') + '/assets' + pathToLocaleFile));
} else {
next(null, false);
}
},
], next);
},
@@ -255,7 +270,7 @@ module.exports = function (middleware) {
data.templateValues.useCustomJS = meta.config.useCustomJS;
data.templateValues.customJS = data.templateValues.useCustomJS ? meta.config.customJS : '';
data.templateValues.isSpider = req.isSpider();
data.templateValues.isSpider = req.uid === -1;
req.app.render('footer', data.templateValues, next);
},
], callback);

View File

@@ -8,7 +8,7 @@ var meta = require('../meta');
var languages = require('../languages');
module.exports = function (middleware) {
middleware.addHeaders = function (req, res, next) {
middleware.addHeaders = function addHeaders(req, res, next) {
var headers = {
'X-Powered-By': encodeURI(meta.config['powered-by'] || 'NodeBB'),
'X-Frame-Options': meta.config['allow-from-uri'] ? 'ALLOW-FROM ' + encodeURI(meta.config['allow-from-uri']) : 'SAMEORIGIN',
@@ -64,7 +64,7 @@ module.exports = function (middleware) {
};
let langs = [];
middleware.autoLocale = function (req, res, next) {
middleware.autoLocale = function autoLocale(req, res, next) {
if (parseInt(req.uid, 10) > 0 || !meta.config.autoDetectLang) {
return next();
}

View File

@@ -21,7 +21,7 @@ var controllers = {
helpers: require('../controllers/helpers'),
};
var delayCache = LRU({
var delayCache = new LRU({
maxAge: 1000 * 60,
});
@@ -42,7 +42,7 @@ require('./maintenance')(middleware);
require('./user')(middleware);
require('./headers')(middleware);
middleware.stripLeadingSlashes = function (req, res, next) {
middleware.stripLeadingSlashes = function stripLeadingSlashes(req, res, next) {
var target = req.originalUrl.replace(nconf.get('relative_path'), '');
if (target.startsWith('//')) {
res.redirect(nconf.get('relative_path') + target.replace(/^\/+/, '/'));
@@ -51,7 +51,7 @@ middleware.stripLeadingSlashes = function (req, res, next) {
}
};
middleware.pageView = function (req, res, next) {
middleware.pageView = function pageView(req, res, next) {
analytics.pageView({
ip: req.ip,
uid: req.uid,
@@ -73,7 +73,7 @@ middleware.pageView = function (req, res, next) {
};
middleware.pluginHooks = function (req, res, next) {
middleware.pluginHooks = function pluginHooks(req, res, next) {
async.each(plugins.loadedHooks['filter:router.page'] || [], function (hookObj, next) {
hookObj.method(req, res, next);
}, function (err) {
@@ -82,7 +82,7 @@ middleware.pluginHooks = function (req, res, next) {
});
};
middleware.validateFiles = function (req, res, next) {
middleware.validateFiles = function validateFiles(req, res, next) {
if (!Array.isArray(req.files.files) || !req.files.files.length) {
return next(new Error(['[[error:invalid-files]]']));
}
@@ -90,12 +90,12 @@ middleware.validateFiles = function (req, res, next) {
next();
};
middleware.prepareAPI = function (req, res, next) {
middleware.prepareAPI = function prepareAPI(req, res, next) {
res.locals.isAPI = true;
next();
};
middleware.routeTouchIcon = function (req, res) {
middleware.routeTouchIcon = function routeTouchIcon(req, res) {
if (meta.config['brand:touchIcon'] && validator.isURL(meta.config['brand:touchIcon'])) {
return res.redirect(meta.config['brand:touchIcon']);
}
@@ -111,7 +111,7 @@ middleware.routeTouchIcon = function (req, res) {
});
};
middleware.privateTagListing = function (req, res, next) {
middleware.privateTagListing = function privateTagListing(req, res, next) {
if (!req.loggedIn && meta.config.privateTagListing) {
controllers.helpers.notAllowed(req, res);
} else {
@@ -119,11 +119,11 @@ middleware.privateTagListing = function (req, res, next) {
}
};
middleware.exposeGroupName = function (req, res, next) {
middleware.exposeGroupName = function exposeGroupName(req, res, next) {
expose('groupName', groups.getGroupNameByGroupSlug, 'slug', req, res, next);
};
middleware.exposeUid = function (req, res, next) {
middleware.exposeUid = function exposeUid(req, res, next) {
expose('uid', user.getUidByUserslug, 'userslug', req, res, next);
};
@@ -142,7 +142,7 @@ function expose(exposedField, method, field, req, res, next) {
], next);
}
middleware.privateUploads = function (req, res, next) {
middleware.privateUploads = function privateUploads(req, res, next) {
if (req.loggedIn || !meta.config.privateUploads) {
return next();
}
@@ -158,7 +158,7 @@ middleware.privateUploads = function (req, res, next) {
next();
};
middleware.busyCheck = function (req, res, next) {
middleware.busyCheck = function busyCheck(req, res, next) {
if (global.env === 'production' && meta.config.eventLoopCheckEnabled && toobusy()) {
analytics.increment('errors:503');
res.status(503).type('text/html').sendFile(path.join(__dirname, '../../public/503.html'));
@@ -167,13 +167,13 @@ middleware.busyCheck = function (req, res, next) {
}
};
middleware.applyBlacklist = function (req, res, next) {
middleware.applyBlacklist = function applyBlacklist(req, res, next) {
meta.blacklist.test(req.ip, function (err) {
next(err);
});
};
middleware.delayLoading = function (req, res, next) {
middleware.delayLoading = function delayLoading(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
@@ -186,7 +186,7 @@ middleware.delayLoading = function (req, res, next) {
setTimeout(next, 1000);
};
middleware.buildSkinAsset = function (req, res, next) {
middleware.buildSkinAsset = function buildSkinAsset(req, res, next) {
// If this middleware is reached, a skin was requested, so it is built on-demand
var target = path.basename(req.originalUrl).match(/(client-[a-z]+)/);
if (target) {
@@ -206,7 +206,7 @@ middleware.buildSkinAsset = function (req, res, next) {
}
};
middleware.trimUploadTimestamps = (req, res, next) => {
middleware.trimUploadTimestamps = function trimUploadTimestamps(req, res, next) {
// Check match
let basename = path.basename(req.path);
if (req.path.startsWith('/uploads/files/') && middleware.regexes.timestampedUpload.test(basename)) {
@@ -214,5 +214,5 @@ middleware.trimUploadTimestamps = (req, res, next) => {
res.header('Content-Disposition', 'inline; filename="' + basename + '"');
}
return next();
next();
};

View File

@@ -6,7 +6,7 @@ var meta = require('../meta');
var user = require('../user');
module.exports = function (middleware) {
middleware.maintenanceMode = function (req, res, callback) {
middleware.maintenanceMode = function maintenanceMode(req, res, callback) {
if (!meta.config.maintenanceMode) {
return setImmediate(callback);
}

View File

@@ -11,10 +11,10 @@ var widgets = require('../widgets');
var utils = require('../utils');
module.exports = function (middleware) {
middleware.processRender = function (req, res, next) {
middleware.processRender = function processRender(req, res, next) {
// res.render post-processing, modified from here: https://gist.github.com/mrlannigan/5051687
var render = res.render;
res.render = function (template, options, fn) {
res.render = function renderOverride(template, options, fn) {
var self = this;
var req = this.req;
var defaultFn = function (err, str) {
@@ -37,7 +37,7 @@ module.exports = function (middleware) {
var templateToRender;
async.waterfall([
function (next) {
options.loggedIn = !!req.uid;
options.loggedIn = req.uid > 0;
options.relative_path = nconf.get('relative_path');
options.template = { name: template };
options.template[template] = true;

View File

@@ -43,21 +43,21 @@ module.exports = function (middleware) {
callback();
}
middleware.authenticate = function (req, res, next) {
middleware.authenticate = function middlewareAuthenticate(req, res, next) {
authenticate(req, res, next, function () {
controllers.helpers.notAllowed(req, res, next);
});
};
middleware.authenticateOrGuest = function (req, res, next) {
middleware.authenticateOrGuest = function authenticateOrGuest(req, res, next) {
authenticate(req, res, next, next);
};
middleware.ensureSelfOrGlobalPrivilege = function (req, res, next) {
middleware.ensureSelfOrGlobalPrivilege = function ensureSelfOrGlobalPrivilege(req, res, next) {
ensureSelfOrMethod(user.isAdminOrGlobalMod, req, res, next);
};
middleware.ensureSelfOrPrivileged = function (req, res, next) {
middleware.ensureSelfOrPrivileged = function ensureSelfOrPrivileged(req, res, next) {
ensureSelfOrMethod(user.isPrivileged, req, res, next);
};
@@ -87,7 +87,7 @@ module.exports = function (middleware) {
], next);
}
middleware.checkGlobalPrivacySettings = function (req, res, next) {
middleware.checkGlobalPrivacySettings = function checkGlobalPrivacySettings(req, res, next) {
if (!req.loggedIn && meta.config.privateUserInfo) {
return middleware.authenticate(req, res, next);
}
@@ -95,7 +95,7 @@ module.exports = function (middleware) {
next();
};
middleware.checkAccountPermissions = function (req, res, next) {
middleware.checkAccountPermissions = function checkAccountPermissions(req, res, next) {
// This middleware ensures that only the requested user and admins can pass
async.waterfall([
function (next) {
@@ -128,8 +128,8 @@ module.exports = function (middleware) {
], next);
};
middleware.redirectToAccountIfLoggedIn = function (req, res, next) {
if (req.session.forceLogin || !req.uid) {
middleware.redirectToAccountIfLoggedIn = function redirectToAccountIfLoggedIn(req, res, next) {
if (req.session.forceLogin || req.uid <= 0) {
return next();
}
@@ -143,7 +143,7 @@ module.exports = function (middleware) {
], next);
};
middleware.redirectUidToUserslug = function (req, res, next) {
middleware.redirectUidToUserslug = function redirectUidToUserslug(req, res, next) {
var uid = parseInt(req.params.uid, 10);
if (uid <= 0) {
return next();
@@ -164,7 +164,7 @@ module.exports = function (middleware) {
], next);
};
middleware.redirectMeToUserslug = function (req, res, next) {
middleware.redirectMeToUserslug = function redirectMeToUserslug(req, res, next) {
var uid = req.uid;
async.waterfall([
function (next) {
@@ -180,7 +180,7 @@ module.exports = function (middleware) {
], next);
};
middleware.isAdmin = function (req, res, next) {
middleware.isAdmin = function isAdmin(req, res, next) {
async.waterfall([
function (next) {
user.isAdministrator(req.uid, next);
@@ -233,7 +233,7 @@ module.exports = function (middleware) {
res.status(403).render('403', { title: '[[global:403.title]]' });
};
middleware.registrationComplete = function (req, res, next) {
middleware.registrationComplete = function registrationComplete(req, res, next) {
// If the user's session contains registration data, redirect the user to complete registration
if (!req.session.hasOwnProperty('registration')) {
return setImmediate(next);
@@ -244,7 +244,7 @@ module.exports = function (middleware) {
controllers.helpers.redirect(res, '/register/complete');
} else {
return setImmediate(next);
setImmediate(next);
}
};
};

View File

@@ -24,6 +24,7 @@ Notifications.baseTypes = [
'notificationType_follow',
'notificationType_new-chat',
'notificationType_group-invite',
'notificationType_group-request-membership',
];
Notifications.privilegedTypes = [
@@ -557,11 +558,9 @@ Notifications.merge = function (notifications, callback) {
case 'notifications:user_posted_to':
case 'notifications:user_flagged_post_in':
case 'notifications:user_flagged_user':
var usernames = set.map(function (notifObj) {
var usernames = _.uniq(set.map(function (notifObj) {
return notifObj && notifObj.user && notifObj.user.username;
}).filter(function (username, idx, array) {
return array.indexOf(username) === idx;
});
}));
var numUsers = usernames.length;
var title = utils.decodeHTMLEntities(notifications[modifyIndex].topicTitle || '');

View File

@@ -84,19 +84,31 @@ module.exports = function (Plugins) {
Plugins.fireHook = function (hook, params, callback) {
callback = typeof callback === 'function' ? callback : function () {};
function done(err, result) {
if (err) {
return callback(err);
}
if (hook !== 'action:plugins.firehook') {
Plugins.fireHook('action:plugins.firehook', { hook: hook, params: params });
}
if (result !== undefined) {
callback(null, result);
} else {
callback();
}
}
var hookList = Plugins.loadedHooks[hook];
var hookType = hook.split(':')[0];
winston.verbose('[plugins/fireHook]', hook);
switch (hookType) {
case 'filter':
fireFilterHook(hook, hookList, params, callback);
fireFilterHook(hook, hookList, params, done);
break;
case 'action':
fireActionHook(hook, hookList, params, callback);
fireActionHook(hook, hookList, params, done);
break;
case 'static':
fireStaticHook(hook, hookList, params, callback);
fireStaticHook(hook, hookList, params, done);
break;
default:
winston.warn('[plugins] Unknown hookType: ' + hookType + ', hook : ' + hook);

View File

@@ -81,15 +81,6 @@ module.exports = function (Plugins) {
}
Plugins.prepareForBuild = function (targets, callback) {
Plugins.cssFiles.length = 0;
Plugins.lessFiles.length = 0;
Plugins.acpLessFiles.length = 0;
Plugins.clientScripts.length = 0;
Plugins.acpScripts.length = 0;
Plugins.soundpacks.length = 0;
Plugins.languageData.languages = [];
Plugins.languageData.namespaces = [];
var map = {
'plugin static dirs': ['staticDirs'],
'requirejs modules': ['modules'],
@@ -101,13 +92,27 @@ module.exports = function (Plugins) {
languages: ['languageData'],
};
var fields = targets.reduce(function (prev, target) {
if (!map[target]) {
return prev;
var fields = _.uniq(_.flatMap(targets, target => map[target] || []));
// clear old data before build
fields.forEach((field) => {
switch (field) {
case 'clientScripts':
case 'acpScripts':
case 'cssFiles':
case 'lessFiles':
case 'acpLessFiles':
Plugins[field].length = 0;
break;
case 'soundpack':
Plugins.soundpacks.length = 0;
break;
case 'languageData':
Plugins.languageData.languages = [];
Plugins.languageData.namespaces = [];
break;
// do nothing for modules and staticDirs
}
return prev.concat(map[target]);
}, []).filter(function (field, i, arr) {
return arr.indexOf(field) === i;
});
winston.verbose('[plugins] loading the following fields from plugin data: ' + fields.join(', '));

View File

@@ -86,12 +86,7 @@ module.exports = function (Posts) {
Posts.hasBookmarked = function (pid, uid, callback) {
if (parseInt(uid, 10) <= 0) {
if (Array.isArray(pid)) {
callback(null, pid.map(() => false));
} else {
callback(null, false);
}
return;
return callback(null, Array.isArray(pid) ? pid.map(() => false) : false);
}
if (Array.isArray(pid)) {

View File

@@ -3,7 +3,7 @@
var LRU = require('lru-cache');
var meta = require('../meta');
var cache = LRU({
var cache = new LRU({
max: meta.config.postCacheSize,
length: function (n) { return n.length; },
maxAge: 0,

View File

@@ -1,11 +1,14 @@
'use strict';
var async = require('async');
const _ = require('lodash');
var db = require('../database');
var user = require('../user');
var meta = require('../meta');
var groups = require('../groups');
var topics = require('../topics');
var categories = require('../categories');
var notifications = require('../notifications');
var privileges = require('../privileges');
var plugins = require('../plugins');
@@ -18,7 +21,7 @@ module.exports = function (Posts) {
user.getUserFields(uid, ['uid', 'reputation', 'postcount'], next);
},
function (userData, next) {
var shouldQueue = meta.config.postQueue && (!userData.uid || userData.reputation < 0 || userData.postcount <= 0);
const shouldQueue = meta.config.postQueue && (!userData.uid || userData.reputation < 0 || userData.postcount <= 0);
plugins.fireHook('filter:post.shouldQueue', {
shouldQueue: shouldQueue,
uid: uid,
@@ -31,6 +34,44 @@ module.exports = function (Posts) {
], callback);
};
function removeQueueNotification(id, callback) {
async.waterfall([
function (next) {
notifications.rescind('post-queue-' + id, next);
},
function (next) {
getParsedObject(id, next);
},
function (data, next) {
if (!data) {
return callback();
}
getCid(data.type, data, next);
},
function (cid, next) {
getNotificationUids(cid, next);
},
function (uids, next) {
uids.forEach(uid => user.notifications.pushCount(uid));
next();
},
], callback);
}
function getNotificationUids(cid, callback) {
async.waterfall([
function (next) {
async.parallel([
async.apply(groups.getMembersOfGroups, ['administrators', 'Global Moderators']),
async.apply(categories.getModeratorUids, [cid]),
], next);
},
function (results, next) {
next(null, _.uniq(_.flattenDeep(results)));
},
], callback);
}
Posts.addToQueue = function (data, callback) {
var type = data.title ? 'topic' : 'reply';
var id = type + '-' + Date.now();
@@ -64,14 +105,21 @@ module.exports = function (Posts) {
path: '/post-queue',
}, next);
},
cid: function (next) {
getCid(type, data, next);
uids: function (next) {
async.waterfall([
function (next) {
getCid(type, data, next);
},
function (cid, next) {
getNotificationUids(cid, next);
},
], next);
},
}, next);
},
function (results, next) {
if (results.notification) {
notifications.pushGroups(results.notification, ['administrators', 'Global Moderators', 'cid:' + results.cid + ':privileges:moderate'], next);
notifications.push(results.notification, results.uids, next);
} else {
next();
}
@@ -127,15 +175,15 @@ module.exports = function (Posts) {
Posts.removeFromQueue = function (id, callback) {
async.waterfall([
function (next) {
removeQueueNotification(id, next);
},
function (next) {
db.sortedSetRemove('post:queue', id, next);
},
function (next) {
db.delete('post:queue:' + id, next);
},
function (next) {
notifications.rescind('post-queue-' + id, next);
},
], callback);
};

View File

@@ -8,7 +8,13 @@ module.exports = function (app, middleware, controllers) {
var router = express.Router();
app.use('/api', router);
router.get('/config', middleware.applyCSRF, controllers.api.getConfig);
router.get('/config', function (req, res, next) {
if (req.uid >= 0) {
middleware.applyCSRF(req, res, next);
} else {
setImmediate(next);
}
}, controllers.api.getConfig);
router.get('/me', middleware.checkGlobalPrivacySettings, controllers.user.getCurrentUser);
router.get('/user/uid/:uid', middleware.checkGlobalPrivacySettings, controllers.user.getUserByUID);

View File

@@ -3,10 +3,10 @@
var async = require('async');
var passport = require('passport');
var passportLocal = require('passport-local').Strategy;
var nconf = require('nconf');
var winston = require('winston');
var controllers = require('../controllers');
var helpers = require('../controllers/helpers');
var plugins = require('../plugins');
var loginStrategies = [];
@@ -14,8 +14,14 @@ var loginStrategies = [];
var Auth = module.exports;
Auth.initialize = function (app, middleware) {
app.use(passport.initialize());
app.use(passport.session());
const passwortInitMiddleware = passport.initialize();
app.use(function passportInitialize(req, res, next) {
passwortInitMiddleware(req, res, next);
});
const passportSessionMiddleware = passport.session();
app.use(function passportSession(req, res, next) {
passportSessionMiddleware(req, res, next);
});
app.use(Auth.setAuthVars);
@@ -23,7 +29,7 @@ Auth.initialize = function (app, middleware) {
Auth.middleware = middleware;
};
Auth.setAuthVars = function (req, res, next) {
Auth.setAuthVars = function setAuthVars(req, res, next) {
var isSpider = req.isSpider();
req.loggedIn = !isSpider && !!req.user;
if (req.user) {
@@ -68,8 +74,12 @@ Auth.reloadRoutes = function (router, callback) {
});
}
router.get(strategy.callbackURL, function (req, res, next) {
// Ensure the passed-back state value is identical to the saved ssoState
router[strategy.callbackMethod || 'get'](strategy.callbackURL, function (req, res, next) {
// Ensure the passed-back state value is identical to the saved ssoState (unless explicitly skipped)
if (strategy.checkState === false) {
return next();
}
next(req.query.state !== req.session.ssoState ? new Error('[[error:csrf-invalid]]') : null);
}, function (req, res, next) {
// Trigger registration interstitial checks
@@ -78,10 +88,27 @@ Auth.reloadRoutes = function (router, callback) {
// passport seems to remove `req.session.returnTo` after it redirects
req.session.registration.returnTo = req.session.returnTo;
next();
}, passport.authenticate(strategy.name, {
successReturnToOrRedirect: nconf.get('relative_path') + (strategy.successUrl !== undefined ? strategy.successUrl : '/'),
failureRedirect: nconf.get('relative_path') + (strategy.failureUrl !== undefined ? strategy.failureUrl : '/login'),
}));
}, function (req, res, next) {
passport.authenticate(strategy.name, function (err, user) {
if (err) {
delete req.session.registration;
return next(err);
}
if (!user) {
delete req.session.registration;
return helpers.redirect(res, strategy.failureUrl !== undefined ? strategy.failureUrl : '/login');
}
req.login(user, function (err) {
if (err) {
return next(err);
}
helpers.redirect(res, strategy.successUrl !== undefined ? strategy.successUrl : '/');
});
})(req, res, next);
});
});
router.post('/register', Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist, controllers.authentication.register);

View File

@@ -35,6 +35,7 @@ function mainRoutes(app, middleware, controllers) {
setupPageRoute(app, '/tos', middleware, [], controllers.termsOfUse);
app.post('/compose', middleware.applyCSRF, controllers.composer.post);
app.post('/email/unsubscribe/:token', controllers.accounts.settings.unsubscribe);
}
function modRoutes(app, middleware, controllers) {
@@ -53,7 +54,9 @@ function topicRoutes(app, middleware, controllers) {
}
function postRoutes(app, middleware, controllers) {
setupPageRoute(app, '/post/:pid', middleware, [], controllers.posts.redirectToPost);
const middlewares = [middleware.maintenanceMode, middleware.registrationComplete, middleware.pluginHooks];
app.get('/post/:pid', middleware.busyCheck, middleware.buildHeader, middlewares, controllers.posts.redirectToPost);
app.get('/api/post/:pid', middlewares, controllers.posts.redirectToPost);
}
function tagRoutes(app, middleware, controllers) {

View File

@@ -330,7 +330,7 @@ function getSearchCids(data, callback) {
async.parallel({
watchedCids: function (next) {
if (data.categories.includes('watched')) {
user.getWatchedCategories(data.uid, next);
user.getCategoriesByStates(data.uid, [categories.watchStates.watching], next);
} else {
setImmediate(next, null, []);
}

View File

@@ -14,6 +14,7 @@ var userDigest = require('../user/digest');
var userEmail = require('../user/email');
var logger = require('../logger');
var events = require('../events');
var notifications = require('../notifications');
var emailer = require('../emailer');
var db = require('../database');
var analytics = require('../analytics');
@@ -273,6 +274,30 @@ SocketAdmin.email.test = function (socket, data, callback) {
}, callback);
break;
case 'notification':
async.waterfall([
function (next) {
notifications.create({
type: 'test',
bodyShort: '[[admin-settings-email:testing]]',
bodyLong: '[[admin-settings-email:testing.send-help]]',
nid: 'uid:' + socket.uid + ':test',
path: '/',
from: socket.uid,
}, next);
},
function (notifObj, next) {
emailer.send('notification', socket.uid, {
path: notifObj.path,
subject: utils.stripHTMLTags(notifObj.subject || '[[notifications:new_notification]]'),
intro: utils.stripHTMLTags(notifObj.bodyShort),
body: notifObj.bodyLong || '',
notification: notifObj,
showUnsubscribe: true,
}, next);
},
]);
break;
default:
emailer.send(data.template, socket.uid, payload, callback);
break;
@@ -333,6 +358,10 @@ SocketAdmin.errors.clear = function (socket, data, callback) {
meta.errors.clear(callback);
};
SocketAdmin.deleteEvents = function (socket, eids, callback) {
events.deleteEvents(eids, callback);
};
SocketAdmin.deleteAllEvents = function (socket, data, callback) {
events.deleteAll(callback);
};

View File

@@ -155,20 +155,28 @@ SocketCategories.getSelectCategories = function (socket, data, callback) {
], callback);
};
SocketCategories.watch = function (socket, cid, callback) {
ignoreOrWatch(user.watchCategory, socket, cid, callback);
SocketCategories.setWatchState = function (socket, data, callback) {
if (!data || !data.cid || !data.state) {
return callback(new Error('[[error:invalid-data]]'));
}
ignoreOrWatch(function (uid, cid, next) {
user.setCategoryWatchState(uid, cid, categories.watchStates[data.state], next);
}, socket, data, callback);
};
SocketCategories.ignore = function (socket, cid, callback) {
ignoreOrWatch(user.ignoreCategory, socket, cid, callback);
SocketCategories.watch = function (socket, data, callback) {
ignoreOrWatch(user.watchCategory, socket, data, callback);
};
function ignoreOrWatch(fn, socket, cid, callback) {
SocketCategories.ignore = function (socket, data, callback) {
ignoreOrWatch(user.ignoreCategory, socket, data, callback);
};
function ignoreOrWatch(fn, socket, data, callback) {
var targetUid = socket.uid;
var cids = [parseInt(cid, 10)];
if (typeof cid === 'object') {
targetUid = cid.uid;
cids = [parseInt(cid.cid, 10)];
var cids = [parseInt(data.cid, 10)];
if (data.hasOwnProperty('uid')) {
targetUid = data.uid;
}
async.waterfall([

View File

@@ -2,12 +2,14 @@
var async = require('async');
var winston = require('winston');
var _ = require('lodash');
var db = require('../database');
var websockets = require('./index');
var user = require('../user');
var posts = require('../posts');
var topics = require('../topics');
var categories = require('../categories');
var privileges = require('../privileges');
var notifications = require('../notifications');
var plugins = require('../plugins');
@@ -16,21 +18,32 @@ var utils = require('../utils');
var SocketHelpers = module.exports;
SocketHelpers.notifyNew = function (uid, type, result) {
let watchStateUids;
let categoryWatchStates;
let topicFollowState;
const post = result.posts[0];
const tid = post.topic.tid;
const cid = post.topic.cid;
async.waterfall([
function (next) {
user.getUidsFromSet('users:online', 0, -1, next);
},
function (uids, next) {
privileges.topics.filterUids('read', result.posts[0].topic.tid, uids, next);
uids = uids.filter(toUid => parseInt(toUid, 10) !== uid);
privileges.topics.filterUids('read', tid, uids, next);
},
function (uids, next) {
filterTidCidIgnorers(uids, result.posts[0].topic.tid, result.posts[0].topic.cid, next);
watchStateUids = uids;
getWatchStates(watchStateUids, tid, cid, next);
},
function (uids, next) {
function (watchStates, next) {
categoryWatchStates = _.zipObject(watchStateUids, watchStates.categoryWatchStates);
topicFollowState = _.zipObject(watchStateUids, watchStates.topicFollowed);
const uids = filterTidCidIgnorers(watchStateUids, watchStates);
user.blocks.filterUids(uid, uids, next);
},
function (uids, next) {
user.blocks.filterUids(result.posts[0].topic.uid, uids, next);
user.blocks.filterUids(post.topic.uid, uids, next);
},
function (uids, next) {
plugins.fireHook('filter:sockets.sendNewPostToUids', { uidsTo: uids, uidFrom: uid, type: type }, next);
@@ -40,42 +53,38 @@ SocketHelpers.notifyNew = function (uid, type, result) {
return winston.error(err.stack);
}
result.posts[0].ip = undefined;
post.ip = undefined;
data.uidsTo.forEach(function (toUid) {
if (parseInt(toUid, 10) !== uid) {
websockets.in('uid_' + toUid).emit('event:new_post', result);
if (result.topic && type === 'newTopic') {
websockets.in('uid_' + toUid).emit('event:new_topic', result.topic);
}
post.categoryWatchState = categoryWatchStates[toUid];
post.topic.isFollowing = topicFollowState[toUid];
websockets.in('uid_' + toUid).emit('event:new_post', result);
if (result.topic && type === 'newTopic') {
websockets.in('uid_' + toUid).emit('event:new_topic', result.topic);
}
});
});
};
function filterTidCidIgnorers(uids, tid, cid, callback) {
async.waterfall([
function (next) {
async.parallel({
topicFollowed: function (next) {
db.isSetMembers('tid:' + tid + ':followers', uids, next);
},
topicIgnored: function (next) {
db.isSetMembers('tid:' + tid + ':ignorers', uids, next);
},
categoryIgnored: function (next) {
db.sortedSetScores('cid:' + cid + ':ignorers', uids, next);
},
}, next);
function getWatchStates(uids, tid, cid, callback) {
async.parallel({
topicFollowed: function (next) {
db.isSetMembers('tid:' + tid + ':followers', uids, next);
},
function (results, next) {
uids = uids.filter(function (uid, index) {
return results.topicFollowed[index] ||
(!results.topicFollowed[index] && !results.topicIgnored[index] && !results.categoryIgnored[index]);
});
next(null, uids);
topicIgnored: function (next) {
db.isSetMembers('tid:' + tid + ':ignorers', uids, next);
},
], callback);
categoryWatchStates: function (next) {
categories.getUidsWatchStates(cid, uids, next);
},
}, callback);
}
function filterTidCidIgnorers(uids, watchStates) {
return uids.filter(function (uid, index) {
return watchStates.topicFollowed[index] ||
(!watchStates.topicIgnored[index] && watchStates.categoryWatchStates[index] !== categories.watchStates.ignoring);
});
}
SocketHelpers.sendNotificationToPostOwner = function (pid, fromuid, command, notification) {

View File

@@ -10,6 +10,7 @@ var cookieParser = require('cookie-parser')(nconf.get('secret'));
var db = require('../database');
var user = require('../user');
var logger = require('../logger');
var plugins = require('../plugins');
var ratelimit = require('../middleware/ratelimit');
@@ -179,12 +180,17 @@ function validateSession(socket, callback) {
if (!req.signedCookies || !req.signedCookies[nconf.get('sessionKey')]) {
return callback();
}
db.sessionStore.get(req.signedCookies[nconf.get('sessionKey')], function (err, sessionData) {
if (err || !sessionData) {
return callback(err || new Error('[[error:invalid-session]]'));
}
callback();
plugins.fireHook('static:sockets.validateSession', {
req: req,
socket: socket,
session: sessionData,
}, callback);
});
}

View File

@@ -188,13 +188,15 @@ Topics.getTopicWithPosts = function (topicData, set, uid, start, stop, reverse,
topicData.bookmark = results.bookmark;
topicData.postSharing = results.postSharing;
topicData.deleter = results.deleter;
topicData.deletedTimestampISO = utils.toISOString(topicData.deletedTimestamp);
if (results.deleter) {
topicData.deletedTimestampISO = utils.toISOString(topicData.deletedTimestamp);
}
topicData.merger = results.merger;
topicData.mergedTimestampISO = utils.toISOString(topicData.mergedTimestamp);
if (results.merger) {
topicData.mergedTimestampISO = utils.toISOString(topicData.mergedTimestamp);
}
topicData.related = results.related || [];
topicData.unreplied = topicData.postcount === 1;
topicData.icons = [];
plugins.fireHook('filter:topic.get', { topic: topicData, uid: uid }, next);
@@ -245,14 +247,14 @@ function getMainPostAndReplies(topic, set, uid, start, stop, reverse, callback)
}
function getDeleter(topicData, callback) {
if (!topicData.deleterUid) {
if (!parseInt(topicData.deleterUid, 10)) {
return setImmediate(callback, null, null);
}
user.getUserFields(topicData.deleterUid, ['username', 'userslug', 'picture'], callback);
}
function getMerger(topicData, callback) {
if (!topicData.mergerUid) {
if (!parseInt(topicData.mergerUid, 10)) {
return setImmediate(callback, null, null);
}
async.waterfall([

View File

@@ -154,7 +154,7 @@ module.exports = function (Topics) {
if (!parentPids.length) {
return setImmediate(callback);
}
parentPids = _.uniq(parentPids);
var parentPosts;
async.waterfall([
async.apply(posts.getPostsFields, parentPids, ['uid']),

View File

@@ -232,13 +232,13 @@ module.exports = function (Topics) {
};
Topics.getTagData = function (tags, callback) {
var keys = tags.map(function (tag) {
return 'tag:' + tag.value;
});
if (!tags.length) {
return setImmediate(callback, null, []);
}
async.waterfall([
function (next) {
db.getObjects(keys, next);
db.getObjects(tags.map(tag => 'tag:' + tag.value), next);
},
function (tagData, next) {
tags.forEach(function (tag, index) {

View File

@@ -64,15 +64,37 @@ module.exports = function (Topics) {
};
Topics.getUnreadTids = function (params, callback) {
var uid = parseInt(params.uid, 10);
var counts = {
async.waterfall([
function (next) {
Topics.getUnreadData(params, next);
},
function (results, next) {
next(null, params.count ? results.counts : results.tids);
},
], callback);
};
Topics.getUnreadData = function (params, callback) {
const uid = parseInt(params.uid, 10);
const counts = {
'': 0,
new: 0,
watched: 0,
unreplied: 0,
};
const noUnreadData = {
tids: [],
counts: counts,
tidsByFilter: {
'': [],
new: [],
watched: [],
unreplied: [],
},
};
if (uid <= 0) {
return callback(null, params.count ? counts : []);
return setImmediate(callback, null, noUnreadData);
}
params.filter = params.filter || '';
@@ -102,7 +124,7 @@ module.exports = function (Topics) {
},
function (results, next) {
if (results.recentTids && !results.recentTids.length && !results.tids_unread.length) {
return callback(null, params.count ? counts : []);
return callback(null, noUnreadData);
}
filterTopics(params, results, next);
@@ -117,9 +139,6 @@ module.exports = function (Topics) {
filter: params.filter,
}, next);
},
function (results, next) {
next(null, params.count ? results.counts : results.tids);
},
], callback);
};
@@ -166,7 +185,7 @@ module.exports = function (Topics) {
tids = tids.slice(0, 200);
if (!tids.length) {
return callback(null, { counts: counts, tids: tids });
return callback(null, { counts: counts, tids: tids, tidsByFilter: tidsByFilter });
}
async.waterfall([
@@ -194,8 +213,8 @@ module.exports = function (Topics) {
isTopicsFollowed: function (next) {
db.sortedSetScores('uid:' + uid + ':followed_tids', tids, next);
},
ignoredCids: function (next) {
categories.isIgnored(cids, uid, next);
categoryWatchState: function (next) {
categories.getWatchState(cids, uid, next);
},
readableCids: function (next) {
privileges.categories.filterCids('read', cids, uid, next);
@@ -205,7 +224,7 @@ module.exports = function (Topics) {
function (results, next) {
cid = cid && cid.map(String);
results.readableCids = results.readableCids.map(String);
const isCidIgnored = _.zipObject(cids, results.ignoredCids);
const userCidState = _.zipObject(cids, results.categoryWatchState);
topicData.forEach(function (topic, index) {
function cidMatch(topicCid) {
@@ -214,7 +233,7 @@ module.exports = function (Topics) {
if (topic && topic.cid && cidMatch(topic.cid) && !blockedUids.includes(parseInt(topic.uid, 10))) {
topic.tid = parseInt(topic.tid, 10);
if ((results.isTopicsFollowed[index] || !isCidIgnored[topic.cid])) {
if ((results.isTopicsFollowed[index] || userCidState[topic.cid] === categories.watchStates.watching)) {
tidsByFilter[''].push(topic.tid);
}

View File

@@ -0,0 +1,46 @@
'use strict';
var async = require('async');
var db = require('../../database');
var batch = require('../../batch');
var categories = require('../../categories');
module.exports = {
name: 'Update category watch data',
timestamp: Date.UTC(2018, 11, 13),
method: function (callback) {
const progress = this.progress;
let keys;
async.waterfall([
function (next) {
db.getSortedSetRange('categories:cid', 0, -1, next);
},
function (cids, next) {
keys = cids.map(cid => 'cid:' + cid + ':ignorers');
batch.processSortedSet('users:joindate', function (uids, next) {
progress.incr(uids.length);
async.eachSeries(cids, function (cid, next) {
db.isSortedSetMembers('cid:' + cid + ':ignorers', uids, function (err, isMembers) {
if (err) {
return next(err);
}
uids = uids.filter((uid, index) => isMembers[index]);
if (!uids.length) {
return setImmediate(next);
}
const states = uids.map(() => categories.watchStates.ignoring);
db.sortedSetAdd('cid:' + cid + ':uid:watch:state', states, uids, next);
});
}, next);
}, {
progress: progress,
}, next);
},
function (next) {
db.deleteAll(keys, next);
},
], callback);
},
};

View File

@@ -9,7 +9,7 @@ var pubsub = require('../pubsub');
module.exports = function (User) {
User.blocks = {
_cache: LRU({
_cache: new LRU({
max: 100,
length: function () { return 1; },
maxAge: 0,

View File

@@ -1,15 +1,39 @@
'use strict';
var async = require('async');
const async = require('async');
const _ = require('lodash');
var db = require('../database');
var categories = require('../categories');
const db = require('../database');
const categories = require('../categories');
module.exports = function (User) {
User.getIgnoredCategories = function (uid, callback) {
if (parseInt(uid, 10) <= 0) {
return setImmediate(callback, null, []);
User.setCategoryWatchState = function (uid, cid, state, callback) {
if (!(parseInt(uid, 10) > 0)) {
return setImmediate(callback);
}
const isStateValid = Object.keys(categories.watchStates).some(key => categories.watchStates[key] === parseInt(state, 10));
if (!isStateValid) {
return setImmediate(callback, new Error('[[error:invalid-watch-state]]'));
}
async.waterfall([
function (next) {
categories.exists(cid, next);
},
function (exists, next) {
if (!exists) {
return next(new Error('[[error:no-category]]'));
}
db.sortedSetAdd('cid:' + cid + ':uid:watch:state', state, uid, next);
},
], callback);
};
User.getCategoryWatchState = function (uid, callback) {
if (parseInt(uid, 10) <= 0) {
return setImmediate(callback, null, {});
}
let cids;
async.waterfall([
function (next) {
@@ -17,69 +41,49 @@ module.exports = function (User) {
},
function (_cids, next) {
cids = _cids;
db.isMemberOfSortedSets(cids.map(cid => 'cid:' + cid + ':ignorers'), uid, next);
categories.getWatchState(cids, uid, next);
},
function (isMembers, next) {
next(null, cids.filter((cid, index) => isMembers[index]));
function (states, next) {
next(null, _.zipObject(cids, states));
},
], callback);
};
User.getIgnoredCategories = function (uid, callback) {
if (parseInt(uid, 10) <= 0) {
return setImmediate(callback, null, []);
}
User.getCategoriesByStates(uid, [categories.watchStates.ignoring], callback);
};
User.getWatchedCategories = function (uid, callback) {
if (parseInt(uid, 10) <= 0) {
return setImmediate(callback, null, []);
}
User.getCategoriesByStates(uid, [categories.watchStates.watching], callback);
};
User.getCategoriesByStates = function (uid, states, callback) {
if (!(parseInt(uid, 10) > 0)) {
return categories.getAllCidsFromSet('categories:cid', callback);
}
async.waterfall([
function (next) {
async.parallel({
ignored: function (next) {
User.getIgnoredCategories(uid, next);
},
all: function (next) {
categories.getAllCidsFromSet('categories:cid', next);
},
}, next);
User.getCategoryWatchState(uid, next);
},
function (results, next) {
const ignored = new Set(results.ignored);
const watched = results.all.filter(cid => cid && !ignored.has(String(cid)));
next(null, watched);
function (userState, next) {
const cids = Object.keys(userState);
next(null, cids.filter(cid => states.includes(userState[cid])));
},
], callback);
};
User.ignoreCategory = function (uid, cid, callback) {
if (uid <= 0) {
return setImmediate(callback);
}
async.waterfall([
function (next) {
categories.exists(cid, next);
},
function (exists, next) {
if (!exists) {
return next(new Error('[[error:no-category]]'));
}
db.sortedSetAdd('cid:' + cid + ':ignorers', Date.now(), uid, next);
},
], callback);
User.setCategoryWatchState(uid, cid, categories.watchStates.ignoring, callback);
};
User.watchCategory = function (uid, cid, callback) {
if (uid <= 0) {
return callback();
}
async.waterfall([
function (next) {
categories.exists(cid, next);
},
function (exists, next) {
if (!exists) {
return next(new Error('[[error:no-category]]'));
}
db.sortedSetRemove('cid:' + cid + ':ignorers', uid, next);
},
], callback);
User.setCategoryWatchState(uid, cid, categories.watchStates.watching, callback);
};
};

View File

@@ -291,32 +291,10 @@ User.getModeratorUids = function (callback) {
async.waterfall([
async.apply(categories.getAllCidsFromSet, 'categories:cid'),
function (cids, next) {
var groupNames = cids.reduce(function (memo, cid) {
memo.push('cid:' + cid + ':privileges:moderate');
memo.push('cid:' + cid + ':privileges:groups:moderate');
return memo;
}, []);
groups.getMembersOfGroups(groupNames, next);
categories.getModeratorUids(cids, next);
},
function (memberSets, next) {
// Every other set is actually a list of user groups, not uids, so convert those to members
var sets = memberSets.reduce(function (memo, set, idx) {
if (idx % 2) {
memo.working.push(set);
} else {
memo.regular.push(set);
}
return memo;
}, { working: [], regular: [] });
groups.getMembersOfGroups(sets.working, function (err, memberSets) {
next(err, sets.regular.concat(memberSets || []));
});
},
function (memberSets, next) {
next(null, _.union.apply(_, memberSets));
function (uids, next) {
next(null, _.union(uids));
},
], callback);
};

View File

@@ -5,6 +5,7 @@ var async = require('async');
var db = require('../database');
var topics = require('../topics');
var plugins = require('../plugins');
var meta = require('../meta');
module.exports = function (User) {
User.updateLastOnlineTime = function (uid, callback) {
@@ -52,7 +53,7 @@ module.exports = function (User) {
},
function (lastonline, next) {
function checkOnline(lastonline) {
return now - lastonline < 300000;
return (now - lastonline) < (meta.config.onlineCutoff * 60000);
}
var isOnline;

View File

@@ -3,7 +3,6 @@
var async = require('async');
var winston = require('winston');
var plugins = require('../plugins');
var file = require('../file');
var image = require('../image');
var meta = require('../meta');
@@ -58,7 +57,7 @@ module.exports = function (User) {
var extension = file.typeToExtension(type);
var filename = generateProfileImageFilename(data.uid, 'profilecover', extension);
uploadProfileOrCover(filename, picture, next);
image.uploadImage(filename, 'profile', picture, next);
},
function (uploadData, next) {
url = uploadData.url;
@@ -130,7 +129,7 @@ module.exports = function (User) {
},
function (next) {
var filename = generateProfileImageFilename(data.uid, 'profileavatar', extension);
uploadProfileOrCover(filename, picture, next);
image.uploadImage(filename, 'profile', picture, next);
},
function (_uploadedImage, next) {
uploadedImage = _uploadedImage;
@@ -162,41 +161,12 @@ module.exports = function (User) {
], callback);
}
function uploadProfileOrCover(filename, image, callback) {
if (plugins.hasListeners('filter:uploadImage')) {
return plugins.fireHook('filter:uploadImage', {
image: image,
uid: image.uid,
}, callback);
}
saveFileToLocal(filename, image, callback);
}
function generateProfileImageFilename(uid, type, extension) {
var keepAllVersions = meta.config['profile:keepAllUserImages'] === 1;
var convertToPNG = meta.config['profile:convertProfileImageToPNG'] === 1;
return uid + '-' + type + (keepAllVersions ? '-' + Date.now() : '') + (convertToPNG ? '.png' : extension);
}
function saveFileToLocal(filename, image, callback) {
async.waterfall([
function (next) {
file.isFileTypeAllowed(image.path, next);
},
function (next) {
file.saveFileToLocal(filename, 'profile', image.path, next);
},
function (upload, next) {
next(null, {
url: upload.url,
path: upload.path,
name: image.name,
});
},
], callback);
}
User.removeCoverPicture = function (data, callback) {
db.deleteObjectFields('user:' + data.uid, ['cover:url', 'cover:position'], callback);
};

View File

@@ -79,6 +79,7 @@ module.exports = function (User) {
settings.topicSearchEnabled = parseInt(getSetting(settings, 'topicSearchEnabled', 0), 10) === 1;
settings.bootswatchSkin = settings.bootswatchSkin || '';
settings.scrollToMyPost = parseInt(getSetting(settings, 'scrollToMyPost', 1), 10) === 1;
settings.categoryWatchState = getSetting(settings, 'categoryWatchState', 'notwatching');
notifications.getAllNotificationTypes(next);
},
@@ -137,6 +138,7 @@ module.exports = function (User) {
outgoingChatSound: data.outgoingChatSound,
upvoteNotifFreq: data.upvoteNotifFreq,
bootswatchSkin: data.bootswatchSkin,
categoryWatchState: data.categoryWatchState,
};
async.waterfall([

View File

@@ -13,7 +13,7 @@
<!-- ENDIF !events.length -->
<div class="events-list">
<!-- BEGIN events -->
<div>
<div data-eid="{events.eid}">
<span>#{events.eid} </span><span class="label label-info">{events.type}</span>
<a href="{config.relative_path}/user/{events.user.userslug}" target="_blank">
<!-- IF events.user.picture -->
@@ -23,6 +23,7 @@
<!-- ENDIF events.user.picture -->
</a>
<a href="{config.relative_path}/user/{events.user.userslug}" target="_blank">{events.user.username}</a> (uid {events.uid}) (IP {events.ip})
<span class="pull-right delete-event"><i class="fa fa-trash-o"></i></span>
<span class="pull-right">{events.timestampISO}</span>
<br /><br />
<pre>{events.jsonString}</pre>

View File

@@ -1,4 +1,4 @@
<form role="form" class="category" data-cid="{category.cid}">
<div class="category" data-cid="{category.cid}">
<div class="row">
<div class="col-md-3 pull-right">
<select id="category-selector" class="form-control">
@@ -163,7 +163,7 @@
</div>
</div>
</div>
</form>
</div>
<button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
<i class="material-icons">save</i>

View File

@@ -54,7 +54,7 @@
<p class="help-block">
[[admin/settings/group:default-cover-help]]
</p>
<input type="text" class="form-control input-lg" id="groups:defaultCovers" data-field="groups:defaultCovers" data-field-type="tagsinput" value="{config.relative_path}/assets/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" /><br />
<input type="text" class="form-control input-lg" id="groups:defaultCovers" data-field="groups:defaultCovers" data-field-type="tagsinput" value="/assets/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" /><br />
</form>
</div>
</div>

View File

@@ -178,7 +178,7 @@
<p class="help-block">
[[admin/settings/uploads:default-covers-help]]
</p>
<input type="text" class="form-control input-lg" id="profile:defaultCovers" data-field="profile:defaultCovers" data-field-type="tagsinput" value="{config.relative_path}/assets/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" />
<input type="text" class="form-control input-lg" id="profile:defaultCovers" data-field="profile:defaultCovers" data-field-type="tagsinput" value="/assets/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" />
</form>
</div>
</div>

View File

@@ -307,6 +307,15 @@
</label>
</div>
<div class="form-group">
<label>[[admin/settings/user:categoryWatchState]]</label>
<select class="form-control" data-field="categoryWatchState">
<option value="watching">[[admin/settings/user:categoryWatchState.watching]]</option>
<option value="notwatching">[[admin/settings/user:categoryWatchState.notwatching]]</option>
<option value="ignoring">[[admin/settings/user:categoryWatchState.ignoring]]</option>
</select>
</div>
<label>[[admin/settings/user:default-notification-settings]]</label>
<!-- BEGIN notificationSettings -->

View File

@@ -166,16 +166,22 @@ function setupExpressApp(app, callback) {
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cookieParser());
app.use(useragent.express());
app.use(detector.middleware());
const userAgentMiddleware = useragent.express();
app.use(function userAgent(req, res, next) {
userAgentMiddleware(req, res, next);
});
const spiderDetectorMiddleware = detector.middleware();
app.use(function spiderDetector(req, res, next) {
spiderDetectorMiddleware(req, res, next);
});
app.use(session({
store: db.sessionStore,
secret: nconf.get('secret'),
key: nconf.get('sessionKey'),
cookie: setupCookie(),
resave: true,
saveUninitialized: true,
resave: nconf.get('sessionResave') || false,
saveUninitialized: nconf.get('sessionSaveUninitialized') || false,
}));
var hsts_option = {