This commit is contained in:
akhoury
2016-02-28 14:52:32 -05:00
392 changed files with 3010 additions and 2225 deletions

View File

@@ -1,23 +1,37 @@
'use strict';
var cronJob = require('cron').CronJob;
var async = require('async');
var db = require('./database');
(function(Analytics) {
var counters = {};
var pageViews = 0;
var uniqueIPCount = 0;
var uniquevisitors = 0;
var isCategory = /^(?:\/api)?\/category\/(\d+)/;
new cronJob('*/10 * * * *', function() {
Analytics.writeData();
}, null, true);
Analytics.pageView = function(ip) {
Analytics.increment = function(keys) {
keys = Array.isArray(keys) ? keys : [keys];
keys.forEach(function(key) {
counters[key] = counters[key] || 0;
++counters[key];
});
};
Analytics.pageView = function(payload) {
++pageViews;
if (ip) {
db.sortedSetScore('ip:recent', ip, function(err, score) {
if (payload.ip) {
db.sortedSetScore('ip:recent', payload.ip, function(err, score) {
if (err) {
return;
}
@@ -28,40 +42,115 @@ var db = require('./database');
today.setHours(today.getHours(), 0, 0, 0);
if (!score || score < today.getTime()) {
++uniquevisitors;
db.sortedSetAdd('ip:recent', Date.now(), ip);
db.sortedSetAdd('ip:recent', Date.now(), payload.ip);
}
});
}
if (payload.path) {
var categoryMatch = payload.path.match(isCategory),
cid = categoryMatch ? parseInt(categoryMatch[1], 10) : null;
if (cid) {
Analytics.increment(['pageviews:byCid:' + cid]);
}
}
};
Analytics.writeData = function() {
var today = new Date();
var month = new Date();
var dbQueue = [];
var today;
if (pageViews > 0 || uniquevisitors > 0) {
today = new Date();
today.setHours(today.getHours(), 0, 0, 0);
}
today.setHours(today.getHours(), 0, 0, 0);
month.setMonth(month.getMonth(), 1);
month.setHours(0, 0, 0, 0);
if (pageViews > 0) {
db.sortedSetIncrBy('analytics:pageviews', pageViews, today.getTime());
var month = new Date();
month.setMonth(month.getMonth(), 1);
month.setHours(0, 0, 0, 0);
db.sortedSetIncrBy('analytics:pageviews:month', pageViews, month.getTime());
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews', pageViews, today.getTime()));
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:pageviews:month', pageViews, month.getTime()));
pageViews = 0;
}
if (uniquevisitors > 0) {
db.sortedSetIncrBy('analytics:uniquevisitors', uniquevisitors, today.getTime());
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:uniquevisitors', uniquevisitors, today.getTime()));
uniquevisitors = 0;
}
if (uniqueIPCount > 0) {
db.incrObjectFieldBy('global', 'uniqueIPCount', uniqueIPCount);
dbQueue.push(async.apply(db.incrObjectFieldBy, 'global', 'uniqueIPCount', uniqueIPCount));
uniqueIPCount = 0;
}
if (Object.keys(counters).length > 0) {
for(var key in counters) {
dbQueue.push(async.apply(db.sortedSetIncrBy, 'analytics:' + key, counters[key], today.getTime()));
delete counters[key];
}
}
async.parallel(dbQueue, function(err) {
if (err) {
winston.error('[analytics] Encountered error while writing analytics to data store: ' + err.message);
}
});
};
Analytics.getHourlyStatsForSet = function(set, hour, numHours, callback) {
var terms = {},
hoursArr = [];
hour = new Date(hour);
hour.setHours(hour.getHours(), 0, 0, 0);
for (var i = 0, ii = numHours; i < ii; i++) {
hoursArr.push(hour.getTime());
hour.setHours(hour.getHours() - 1, 0, 0, 0);
}
db.sortedSetScores(set, hoursArr, function(err, counts) {
if (err) {
return callback(err);
}
hoursArr.forEach(function(term, index) {
terms[term] = parseInt(counts[index], 10) || 0;
});
var termsArr = [];
hoursArr.reverse();
hoursArr.forEach(function(hour) {
termsArr.push(terms[hour]);
});
callback(null, termsArr);
});
};
Analytics.getDailyStatsForSet = function(set, day, numDays, callback) {
var daysArr = [];
day = new Date(day);
day.setDate(day.getDate()+1); // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values
day.setHours(0, 0, 0, 0);
async.whilst(function() {
return numDays--;
}, function(next) {
Analytics.getHourlyStatsForSet(set, day.getTime()-(1000*60*60*24*numDays), 24, function(err, day) {
if (err) {
return next(err);
}
daysArr.push(day.reduce(function(cur, next) {
return cur+next;
}));
next();
});
}, function(err) {
callback(err, daysArr);
});
};
Analytics.getUnwrittenPageviews = function() {
@@ -86,4 +175,13 @@ var db = require('./database');
});
};
Analytics.getCategoryAnalytics = function(cid, callback) {
async.parallel({
'pageviews:hourly': async.apply(Analytics.getHourlyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 24),
'pageviews:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 30),
'topics:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:topics:byCid:' + cid, Date.now(), 7),
'posts:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:posts:byCid:' + cid, Date.now(), 7),
}, callback);
};
}(exports));

View File

@@ -1,15 +1,13 @@
'use strict';
var async = require('async'),
nconf = require('nconf'),
var async = require('async');
db = require('./database'),
user = require('./user'),
Groups = require('./groups'),
plugins = require('./plugins'),
validator = require('validator'),
privileges = require('./privileges');
var db = require('./database');
var user = require('./user');
var Groups = require('./groups');
var plugins = require('./plugins');
var privileges = require('./privileges');
(function(Categories) {
@@ -27,37 +25,40 @@ var async = require('async'),
};
Categories.getCategoryById = function(data, callback) {
Categories.getCategories([data.cid], data.uid, function(err, categories) {
if (err || !Array.isArray(categories) || !categories[0]) {
return callback(err || new Error('[[error:invalid-cid]]'));
}
var category = categories[0];
if (parseInt(data.uid, 10)) {
Categories.markAsRead([data.cid], data.uid);
}
async.parallel({
topics: function(next) {
Categories.getCategoryTopics(data, next);
},
isIgnored: function(next) {
Categories.isIgnored([data.cid], data.uid, next);
var category;
async.waterfall([
function (next) {
Categories.getCategories([data.cid], data.uid, next);
},
function (categories, next) {
if (!Array.isArray(categories) || !categories[0]) {
return next(new Error('[[error:invalid-cid]]'));
}
}, function(err, results) {
if(err) {
return callback(err);
category = categories[0];
if (parseInt(data.uid, 10)) {
Categories.markAsRead([data.cid], data.uid);
}
async.parallel({
topics: function(next) {
Categories.getCategoryTopics(data, next);
},
isIgnored: function(next) {
Categories.isIgnored([data.cid], data.uid, next);
}
}, next);
},
function (results, next) {
category.topics = results.topics.topics;
category.nextStart = results.topics.nextStart;
category.isIgnored = results.isIgnored[0];
plugins.fireHook('filter:category.get', {category: category, uid: data.uid}, function(err, data) {
callback(err, data ? data.category : null);
});
});
});
plugins.fireHook('filter:category.get', {category: category, uid: data.uid}, next);
},
function (data, next) {
next(null, data.category);
}
], callback);
};
Categories.isIgnored = function(cids, uid, callback) {

View File

@@ -8,10 +8,10 @@ var async = require('async'),
module.exports = function(Categories) {
Categories.purge = function(cid, callback) {
Categories.purge = function(cid, uid, callback) {
batch.processSortedSet('cid:' + cid + ':tids', function(tids, next) {
async.eachLimit(tids, 10, function(tid, next) {
topics.purgePostsAndTopic(tid, next);
topics.purgePostsAndTopic(tid, uid, next);
}, next);
}, {alwaysStartAt: 0}, function(err) {
if (err) {

View File

@@ -47,6 +47,7 @@ module.exports = function(Categories) {
topic.slug = topic.tid;
topic.teaser = null;
topic.noAnchor = true;
topic.tags = [];
}
});
};

View File

@@ -8,6 +8,7 @@ var winston = require('winston');
var db = require('../../database');
var user = require('../../user');
var meta = require('../../meta');
var plugins = require('../../plugins');
var helpers = require('../helpers');
var accountHelpers = require('./helpers');
@@ -28,8 +29,15 @@ editController.get = function(req, res, callback) {
userData.title = '[[pages:account/edit, ' + userData.username + ']]';
userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:edit]]'}]);
userData.editButtons = [];
res.render('account/edit', userData);
plugins.fireHook('filter:user.account.edit', userData, function(err, userData) {
if (err) {
return next(err);
}
res.render('account/edit', userData);
});
});
};

View File

@@ -36,8 +36,10 @@ profileController.get = function(req, res, callback) {
}
userData = _userData;
if (req.uid !== parseInt(userData.uid, 10)) {
req.session.uids_viewed = req.session.uids_viewed || {};
if (req.uid !== parseInt(userData.uid, 10) && (!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,17 +1,17 @@
'use strict';
var async = require('async'),
var async = require('async');
user = require('../../user'),
groups = require('../../groups'),
languages = require('../../languages'),
meta = require('../../meta'),
plugins = require('../../plugins'),
privileges = require('../../privileges'),
categories = require('../../categories'),
db = require('../../database'),
helpers = require('../helpers'),
accountHelpers = require('./helpers');
var user = require('../../user');
var groups = require('../../groups');
var languages = require('../../languages');
var meta = require('../../meta');
var plugins = require('../../plugins');
var privileges = require('../../privileges');
var categories = require('../../categories');
var db = require('../../database');
var helpers = require('../helpers');
var accountHelpers = require('./helpers');
var settingsController = {};
@@ -41,14 +41,18 @@ settingsController.get = function(req, res, callback) {
homePageRoutes: function(next) {
getHomePageRoutes(next);
},
ips: function (next) {
user.getIPs(req.uid, 4, next);
},
sessions: async.apply(user.auth.getSessions, userData.uid, req.sessionID)
}, next);
},
function(results, next) {
userData.settings = results.settings;
userData.languages = results.languages;
userData.userGroups = results.userGroups[0];
userData.languages = results.languages;
userData.homePageRoutes = results.homePageRoutes;
userData.ips = results.ips;
userData.sessions = results.sessions;
plugins.fireHook('filter:user.customSettings', {settings: results.settings, customSettings: [], uid: req.uid}, next);
},

View File

@@ -22,6 +22,7 @@ var adminController = {
sounds: require('./admin/sounds'),
homepage: require('./admin/homepage'),
navigation: require('./admin/navigation'),
social: require('./admin/social'),
themes: require('./admin/themes'),
users: require('./admin/users'),
uploads: require('./admin/uploads'),

View File

@@ -4,6 +4,7 @@ var async = require('async'),
categories = require('../../categories'),
privileges = require('../../privileges'),
analytics = require('../../analytics'),
plugins = require('../../plugins');
@@ -12,20 +13,22 @@ var categoriesController = {};
categoriesController.get = function(req, res, next) {
async.parallel({
category: async.apply(categories.getCategories, [req.params.category_id], req.user.uid),
privileges: async.apply(privileges.categories.list, req.params.category_id)
privileges: async.apply(privileges.categories.list, req.params.category_id),
analytics: async.apply(analytics.getCategoryAnalytics, req.params.category_id)
}, function(err, data) {
if (err) {
return next(err);
}
plugins.fireHook('filter:admin.category.get', {req: req, res: res, category: data.category[0], privileges: data.privileges}, function(err, data) {
plugins.fireHook('filter:admin.category.get', { req: req, res: res, category: data.category[0], privileges: data.privileges, analytics: data.analytics }, function(err, data) {
if (err) {
return next(err);
}
res.render('admin/manage/category', {
category: data.category,
privileges: data.privileges
privileges: data.privileges,
analytics: data.analytics
});
});
});

View File

@@ -0,0 +1,20 @@
'use strict';
var social = require('../../social');
var socialController = {};
socialController.get = function(req, res, next) {
social.getPostSharing(function(err, posts) {
if (err) {
return next(err);
}
res.render('admin/general/social', {
posts: posts
});
});
};
module.exports = socialController;

View File

@@ -93,6 +93,33 @@ uploadsController.uploadLogo = function(req, res, next) {
upload('site-logo', req, res, next);
};
uploadsController.uploadSound = function(req, res, next) {
var uploadedFile = req.files.files[0];
file.saveFileToLocal(uploadedFile.name, 'sounds', uploadedFile.path, function(err) {
if (err) {
return next(err);
}
var soundsPath = path.join(__dirname, '../../../public/sounds'),
filePath = path.join(__dirname, '../../../public/uploads/sounds', uploadedFile.name);
if (process.platform === 'win32') {
fs.link(filePath, path.join(soundsPath, path.basename(filePath)));
} else {
fs.symlink(filePath, path.join(soundsPath, path.basename(filePath)), 'file');
}
fs.unlink(uploadedFile.path, function(err) {
if (err) {
return next(err);
}
res.json([{}]);
});
});
};
uploadsController.uploadDefaultAvatar = function(req, res, next) {
upload('avatar-default', req, res, next);
};
@@ -131,6 +158,7 @@ function uploadImage(filename, folder, uploadedFile, req, res, next) {
if (err) {
return next(err);
}
res.json([{name: uploadedFile.name, url: image.url.startsWith('http') ? image.url : nconf.get('relative_path') + image.url}]);
}

View File

@@ -87,8 +87,7 @@ function registerAndLoginUser(req, res, userData, callback) {
function(_uid, next) {
uid = _uid;
if (res.locals.processLogin === true) {
user.logIP(uid, req.ip);
req.login({uid: uid}, next);
doLogin(req, uid, next);
} else {
next();
}
@@ -172,37 +171,11 @@ function continueLogin(req, res, next) {
res.status(200).send(nconf.get('relative_path') + '/reset/' + code);
});
} else {
req.login({
uid: userData.uid
}, function(err) {
doLogin(req, userData.uid, function(err) {
if (err) {
return res.status(403).send(err.message);
}
if (userData.uid) {
var uuid = utils.generateUUID();
req.session.meta = {};
// Associate IP used during login with user account
user.logIP(userData.uid, req.ip);
req.session.meta.ip = req.ip;
// Associate metadata retrieved via user-agent
req.session.meta = _.extend(req.session.meta, {
uuid: uuid,
datetime: Date.now(),
platform: req.useragent.platform,
browser: req.useragent.browser,
version: req.useragent.version
});
// Associate login session with user
user.auth.addSession(userData.uid, req.sessionID);
db.setObjectField('uid:' + userData.uid + 'sessionUUID:sessionId', uuid, req.sessionID);
plugins.fireHook('action:user.loggedIn', userData.uid);
}
if (!req.session.returnTo) {
res.status(200).send(nconf.get('relative_path') + '/');
} else {
@@ -216,6 +189,40 @@ function continueLogin(req, res, next) {
})(req, res, next);
}
function doLogin(req, uid, callback) {
req.login({uid: uid}, function(err) {
if (err) {
return callback(err);
}
if (uid) {
var uuid = utils.generateUUID();
req.session.meta = {};
// Associate IP used during login with user account
user.logIP(uid, req.ip);
req.session.meta.ip = req.ip;
// Associate metadata retrieved via user-agent
req.session.meta = _.extend(req.session.meta, {
uuid: uuid,
datetime: Date.now(),
platform: req.useragent.platform,
browser: req.useragent.browser,
version: req.useragent.version
});
// Associate login session with user
user.auth.addSession(uid, req.sessionID);
db.setObjectField('uid:' + uid + 'sessionUUID:sessionId', uuid, req.sessionID);
plugins.fireHook('action:user.loggedIn', uid);
}
callback();
});
}
authenticationController.localLogin = function(req, username, password, next) {
if (!username) {
return next(new Error('[[error:invalid-username]]'));

View File

@@ -182,7 +182,8 @@ Controllers.compose = function(req, res, next) {
Controllers.confirmEmail = function(req, res, next) {
user.email.confirm(req.params.code, function (err) {
res.render('confirm', {
error: err ? err.message : ''
error: err ? err.message : '',
title: '[[pages:confirm]]',
});
});
};

View File

@@ -1,48 +1,44 @@
"use strict";
var tagsController = {},
async = require('async'),
nconf = require('nconf'),
validator = require('validator'),
meta = require('../meta'),
user = require('../user'),
topics = require('../topics'),
helpers = require('./helpers');
var async = require('async');
var nconf = require('nconf');
var validator = require('validator');
var meta = require('../meta');
var topics = require('../topics');
var helpers = require('./helpers');
var tagsController = {};
tagsController.getTag = function(req, res, next) {
var tag = validator.escape(req.params.tag);
var stop = (parseInt(meta.config.topicsPerList, 10) || 20) - 1;
var templateData = {
topics: [],
tag: tag,
breadcrumbs: helpers.buildBreadcrumbs([{text: '[[tags:tags]]', url: '/tags'}, {text: tag}]),
title: '[[pages:tag, ' + tag + ']]'
};
async.waterfall([
function(next) {
function (next) {
topics.getTagTids(req.params.tag, 0, stop, next);
},
function(tids, next) {
function (tids, next) {
if (Array.isArray(tids) && !tids.length) {
topics.deleteTag(req.params.tag);
return res.render('tag', {
topics: [],
tag: tag,
breadcrumbs: helpers.buildBreadcrumbs([{text: '[[tags:tags]]', url: '/tags'}, {text: tag}])
});
return res.render('tag', templateData);
}
async.parallel({
isAdmin: async.apply(user.isAdministrator, req.uid),
topics: async.apply(topics.getTopics, tids, req.uid)
}, next);
topics.getTopics(tids, req.uid, next);
}
], function(err, results) {
], function(err, topics) {
if (err) {
return next(err);
}
if (!results.isAdmin) {
results.topics = results.topics.filter(function(topic) {
return topic && !topic.deleted;
});
}
res.locals.metaTags = [
{
name: 'title',
@@ -57,15 +53,10 @@ tagsController.getTag = function(req, res, next) {
content: nconf.get('url') + '/tags/' + tag
}
];
templateData.topics = topics;
templateData.nextStart = stop + 1;
var data = {
topics: results.topics,
tag: tag,
nextStart: stop + 1,
breadcrumbs: helpers.buildBreadcrumbs([{text: '[[tags:tags]]', url: '/tags'}, {text: tag}]),
title: '[[pages:tag, ' + tag + ']]'
};
res.render('tag', data);
res.render('tag', templateData);
});
};

View File

@@ -1,26 +1,29 @@
"use strict";
var topicsController = {},
async = require('async'),
S = require('string'),
nconf = require('nconf'),
user = require('../user'),
meta = require('../meta'),
topics = require('../topics'),
posts = require('../posts'),
privileges = require('../privileges'),
plugins = require('../plugins'),
helpers = require('./helpers'),
pagination = require('../pagination'),
utils = require('../../public/src/utils');
var async = require('async');
var S = require('string');
var nconf = require('nconf');
var validator = require('validator');
var user = require('../user');
var meta = require('../meta');
var topics = require('../topics');
var posts = require('../posts');
var privileges = require('../privileges');
var plugins = require('../plugins');
var helpers = require('./helpers');
var pagination = require('../pagination');
var utils = require('../../public/src/utils');
var topicsController = {};
topicsController.get = function(req, res, callback) {
var tid = req.params.topic_id,
sort = req.query.sort,
currentPage = parseInt(req.query.page, 10) || 1,
pageCount = 1,
userPrivileges;
var tid = req.params.topic_id;
var sort = req.query.sort;
var currentPage = parseInt(req.query.page, 10) || 1;
var pageCount = 1;
var userPrivileges;
if ((req.params.post_index && !utils.isNumber(req.params.post_index)) || !utils.isNumber(tid)) {
return callback();
@@ -67,8 +70,8 @@ topicsController.get = function(req, res, callback) {
return callback();
}
var set = 'tid:' + tid + ':posts',
reverse = false;
var set = 'tid:' + tid + ':posts';
var reverse = false;
// `sort` qs has priority over user setting
if (sort === 'newest_to_oldest') {
@@ -90,6 +93,7 @@ topicsController.get = function(req, res, callback) {
req.params.post_index = 0;
}
if (!settings.usePagination) {
currentPage = 1;
if (reverse) {
postIndex = Math.max(0, postCount - (req.params.post_index || postCount) - Math.ceil(settings.postsPerPage / 2));
} else {
@@ -187,7 +191,7 @@ topicsController.get = function(req, res, callback) {
},
{
property: 'og:title',
content: topicData.title.replace(/&amp;/g, '&')
content: topicData.title
},
{
property: 'og:description',

View File

@@ -17,14 +17,17 @@ usersController.getOnlineUsers = function(req, res, next) {
if (err) {
return next(err);
}
var hiddenCount = 0;
if (!userData.isAdminOrGlobalMod) {
userData.users = userData.users.filter(function(user) {
if (user && user.status === 'offline') {
hiddenCount ++;
}
return user && user.status !== 'offline';
});
}
userData.anonymousUserCount = require('../socket.io').getOnlineAnonCount();
userData.anonymousUserCount = require('../socket.io').getOnlineAnonCount() + hiddenCount;
render(req, res, userData, next);
});

View File

@@ -1,24 +1,27 @@
"use strict";
var async = require('async'),
winston = require('winston'),
nconf = require('nconf'),
templates = require('templates.js'),
nodemailer = require('nodemailer'),
sendmailTransport = require('nodemailer-sendmail-transport'),
htmlToText = require('html-to-text'),
url = require('url'),
var async = require('async');
var winston = require('winston');
var nconf = require('nconf');
var templates = require('templates.js');
var nodemailer = require('nodemailer');
var sendmailTransport = require('nodemailer-sendmail-transport');
var smtpTransport = require('nodemailer-smtp-transport');
var htmlToText = require('html-to-text');
var url = require('url');
User = require('./user'),
Plugins = require('./plugins'),
meta = require('./meta'),
translator = require('../public/src/modules/translator'),
var User = require('./user');
var Plugins = require('./plugins');
var meta = require('./meta');
var translator = require('../public/src/modules/translator');
transports = {
sendmail: nodemailer.createTransport(sendmailTransport()),
gmail: undefined
},
app, fallbackTransport;
var transports = {
sendmail: nodemailer.createTransport(sendmailTransport()),
gmail: undefined
};
var app;
var fallbackTransport;
(function(Emailer) {
Emailer.registerApp = function(expressApp) {
@@ -26,13 +29,15 @@ var async = require('async'),
// Enable Gmail transport if enabled in ACP
if (parseInt(meta.config['email:GmailTransport:enabled'], 10) === 1) {
fallbackTransport = transports.gmail = nodemailer.createTransport('SMTP', {
service: 'Gmail',
fallbackTransport = transports.gmail = nodemailer.createTransport(smtpTransport({
host: 'smtp.gmail.com',
port: 465,
secure: true,
auth: {
user: meta.config['email:GmailTransport:user'],
pass: meta.config['email:GmailTransport:pass']
}
});
}));
} else {
fallbackTransport = transports.sendmail;
}
@@ -135,28 +140,19 @@ var async = require('async'),
}
function renderAndTranslate(tpl, params, lang, callback) {
async.waterfall([
function(next) {
render('emails/partials/footer' + (tpl.indexOf('_plaintext') !== -1 ? '_plaintext' : ''), params, next);
},
function(footer, next) {
params.footer = footer;
render(tpl, params, next);
},
function(html, next) {
translator.translate(html, lang, function(translated) {
next(null, translated);
});
}
], callback);
render(tpl, params, function(err, html) {
translator.translate(html, lang, function(translated) {
callback(err, translated);
});
});
}
function getHostname() {
var configUrl = nconf.get('url'),
parsed = url.parse(configUrl);
var configUrl = nconf.get('url');
var parsed = url.parse(configUrl);
return parsed.hostname;
};
}
}(module.exports));

View File

@@ -182,7 +182,7 @@ var utils = require('../public/src/utils');
results.base.deleted = !!parseInt(results.base.deleted, 10);
results.base.hidden = !!parseInt(results.base.hidden, 10);
results.base.system = !!parseInt(results.base.system, 10);
results.base.private = results.base.private ? !!parseInt(results.base.private, 10) : true;
results.base.private = (results.base.private === null || results.base.private === undefined) ? true : !!parseInt(results.base.private, 10);
results.base.disableJoinRequests = parseInt(results.base.disableJoinRequests, 10) === 1;
results.base.isMember = results.isMember;
results.base.isPending = results.isPending;
@@ -409,7 +409,7 @@ var utils = require('../public/src/utils');
group.createtimeISO = utils.toISOString(group.createtime);
group.hidden = parseInt(group.hidden, 10) === 1;
group.system = parseInt(group.system, 10) === 1;
group.private = parseInt(group.private, 10) === 1;
group.private = (group.private === null || group.private === undefined) ? true : !!parseInt(group.private, 10);
group.disableJoinRequests = parseInt(group.disableJoinRequests) === 1;
group['cover:url'] = group['cover:url'] || require('./coverPhoto').getDefaultGroupCover(group.name);

View File

@@ -15,19 +15,19 @@ module.exports = function(Groups) {
function join() {
var tasks = [
async.apply(db.sortedSetAdd, 'group:' + groupName + ':members', Date.now(), uid),
async.apply(db.incrObjectField, 'group:' + groupName, 'memberCount')
async.apply(db.incrObjectField, 'group:' + groupName, 'memberCount')
];
async.waterfall([
function(next) {
async.parallel({
isAdmin: function(next) {
user.isAdministrator(uid, next);
user.isAdministrator(uid, next);
},
isHidden: function(next) {
Groups.isHidden(groupName, next);
}
}, next);
}, next);
},
function(results, next) {
if (results.isAdmin) {
@@ -213,10 +213,10 @@ module.exports = function(Groups) {
Groups.destroy(groupName, callback);
} else {
if (parseInt(groupData.hidden, 10) !== 1) {
db.sortedSetAdd('groups:visible:memberCount', groupData.memberCount, groupName, next);
db.sortedSetAdd('groups:visible:memberCount', groupData.memberCount, groupName, callback);
} else {
callback();
}
callback();
}
}
});
});

View File

@@ -359,7 +359,7 @@ var async = require('async'),
if (parseInt(meta.config.disableChat) === 1 || !uid) {
return callback(new Error('[[error:chat-disabled]]'));
}
async.waterfall([
function (next) {
Messaging.isUserInRoom(uid, roomId, next);
@@ -368,6 +368,14 @@ var async = require('async'),
if (!inRoom) {
return next(new Error('[[error:not-in-room]]'));
}
Messaging.getUserCountInRoom(roomId, next);
},
function(count, next) {
if (count < 2) {
return next(new Error('[[error:no-users-in-room]]'));
}
user.getUserFields(uid, ['banned', 'email:confirmed'], next);
},
function (userData, next) {

View File

@@ -17,48 +17,7 @@ module.exports = function(Meta) {
Meta.sounds.init = function(callback) {
if (nconf.get('isPrimary') === 'true') {
var soundsPath = path.join(__dirname, '../../public/sounds');
plugins.fireHook('filter:sounds.get', [], function(err, filePaths) {
if (err) {
winston.error('Could not initialise sound files:' + err.message);
return;
}
// Clear the sounds directory
async.series([
function(next) {
rimraf(soundsPath, next);
},
function(next) {
mkdirp(soundsPath, next);
}
], function(err) {
if (err) {
winston.error('Could not initialise sound files:' + err.message);
return;
}
// Link paths
async.each(filePaths, function(filePath, next) {
if (process.platform === 'win32') {
fs.link(filePath, path.join(soundsPath, path.basename(filePath)), next);
} else {
fs.symlink(filePath, path.join(soundsPath, path.basename(filePath)), 'file', next);
}
}, function(err) {
if (!err) {
winston.verbose('[sounds] Sounds OK');
} else {
winston.error('[sounds] Could not initialise sounds: ' + err.message);
}
if (typeof callback === 'function') {
callback();
}
});
});
});
setupSounds(callback);
} else {
if (typeof callback === 'function') {
callback();
@@ -67,10 +26,23 @@ module.exports = function(Meta) {
};
Meta.sounds.getFiles = function(callback) {
// todo: Possibly move these into a bundled module?
fs.readdir(path.join(__dirname, '../../public/sounds'), function(err, files) {
async.waterfall([
function(next) {
fs.readdir(path.join(__dirname, '../../public/sounds'), next);
},
function(sounds, next) {
fs.readdir(path.join(__dirname, '../../public/uploads/sounds'), function(err, uploaded) {
next(err, sounds.concat(uploaded));
});
}
], function(err, files) {
var localList = {};
// Filter out hidden files
files = files.filter(function(filename) {
return !filename.startsWith('.');
});
if (err) {
winston.error('Could not get local sound files:' + err.message);
console.log(err.stack);
@@ -102,4 +74,60 @@ module.exports = function(Meta) {
callback(null, sounds);
});
};
function setupSounds(callback) {
var soundsPath = path.join(__dirname, '../../public/sounds');
async.waterfall([
function(next) {
fs.readdir(path.join(__dirname, '../../public/uploads/sounds'), next);
},
function(uploaded, next) {
uploaded = uploaded.map(function(filename) {
return path.join(__dirname, '../../public/uploads/sounds', filename);
});
plugins.fireHook('filter:sounds.get', uploaded, function(err, filePaths) {
if (err) {
winston.error('Could not initialise sound files:' + err.message);
return;
}
// Clear the sounds directory
async.series([
function(next) {
rimraf(soundsPath, next);
},
function(next) {
mkdirp(soundsPath, next);
}
], function(err) {
if (err) {
winston.error('Could not initialise sound files:' + err.message);
return;
}
// Link paths
async.each(filePaths, function(filePath, next) {
if (process.platform === 'win32') {
fs.link(filePath, path.join(soundsPath, path.basename(filePath)), next);
} else {
fs.symlink(filePath, path.join(soundsPath, path.basename(filePath)), 'file', next);
}
}, function(err) {
if (!err) {
winston.verbose('[sounds] Sounds OK');
} else {
winston.error('[sounds] Could not initialise sounds: ' + err.message);
}
if (typeof callback === 'function') {
callback();
}
});
});
});
}
], callback);
}
};

View File

@@ -12,7 +12,8 @@ var mkdirp = require('mkdirp'),
plugins = require('../plugins'),
utils = require('../../public/src/utils'),
Templates = {};
Templates = {},
searchIndex = {};
Templates.compile = function(callback) {
callback = callback || function() {};
@@ -27,25 +28,37 @@ Templates.compile = function(callback) {
return callback();
}
var coreTemplatesPath = nconf.get('core_templates_path'),
baseTemplatesPath = nconf.get('base_templates_path'),
viewsPath = nconf.get('views_dir'),
themeTemplatesPath = nconf.get('theme_templates_path'),
themeConfig = require(nconf.get('theme_config'));
compile(callback);
};
if (themeConfig.baseTheme) {
var pathToBaseTheme = path.join(nconf.get('themes_path'), themeConfig.baseTheme);
baseTemplatesPath = require(path.join(pathToBaseTheme, 'theme.json')).templates;
if (!baseTemplatesPath){
baseTemplatesPath = path.join(pathToBaseTheme, 'templates');
}
function getBaseTemplates(theme) {
var baseTemplatesPaths = [],
baseThemePath, baseThemeConfig;
while (theme) {
baseThemePath = path.join(nconf.get('themes_path'), theme);
baseThemeConfig = require(path.join(baseThemePath, 'theme.json'));
baseTemplatesPaths.push(path.join(baseThemePath, baseThemeConfig.templates || 'templates'));
theme = baseThemeConfig.baseTheme;
}
plugins.getTemplates(function(err, pluginTemplates) {
return baseTemplatesPaths.reverse();
}
function preparePaths(baseTemplatesPaths, callback) {
var coreTemplatesPath = nconf.get('core_templates_path'),
viewsPath = nconf.get('views_dir');
async.waterfall([
async.apply(plugins.fireHook, 'static:templates.precompile', {}),
async.apply(plugins.getTemplates)
], function(err, pluginTemplates) {
if (err) {
return callback(err);
}
winston.verbose('[meta/templates] Compiling templates');
rimraf.sync(viewsPath);
mkdirp.sync(viewsPath);
@@ -54,27 +67,33 @@ Templates.compile = function(callback) {
coreTpls: function(next) {
utils.walk(coreTemplatesPath, next);
},
baseTpls: function(next) {
utils.walk(baseTemplatesPath, next);
baseThemes: function(next) {
async.map(baseTemplatesPaths, function(baseTemplatePath, next) {
utils.walk(baseTemplatePath, function(err, paths) {
paths = paths.map(function(tpl) {
return {
base: baseTemplatePath,
path: tpl.replace(baseTemplatePath, '')
};
});
next(err, paths);
});
}, next);
}
}, function(err, data) {
var coreTpls = data.coreTpls,
baseTpls = data.baseTpls,
var baseThemes = data.baseThemes,
coreTpls = data.coreTpls,
paths = {};
if (!baseTpls) {
winston.warn('[meta/templates] Could not find base template files at: ' + baseTemplatesPath);
}
coreTpls = !coreTpls ? [] : coreTpls.map(function(tpl) { return tpl.replace(coreTemplatesPath, ''); });
baseTpls = !baseTpls ? [] : baseTpls.map(function(tpl) { return tpl.replace(baseTemplatesPath, ''); });
coreTpls.forEach(function(el, i) {
paths[coreTpls[i]] = path.join(coreTemplatesPath, coreTpls[i]);
paths[coreTpls[i].replace(coreTemplatesPath, '')] = coreTpls[i];
});
baseTpls.forEach(function(el, i) {
paths[baseTpls[i]] = path.join(baseTemplatesPath, baseTpls[i]);
baseThemes.forEach(function(baseTpls) {
baseTpls.forEach(function(el, i) {
paths[baseTpls[i].path] = path.join(baseTpls[i].base, baseTpls[i].path);
});
});
for (var tpl in pluginTemplates) {
@@ -83,51 +102,65 @@ Templates.compile = function(callback) {
}
}
async.each(Object.keys(paths), function(relativePath, next) {
var file = fs.readFileSync(paths[relativePath]).toString(),
matches = null,
regex = /[ \t]*<!-- IMPORT ([\s\S]*?)? -->[ \t]*/;
callback(err, paths);
});
});
}
while((matches = file.match(regex)) !== null) {
var partial = "/" + matches[1];
function compile(callback) {
var themeConfig = require(nconf.get('theme_config')),
baseTemplatesPaths = themeConfig.baseTheme ? getBaseTemplates(themeConfig.baseTheme) : [nconf.get('base_templates_path')],
viewsPath = nconf.get('views_dir');
if (paths[partial] && relativePath !== partial) {
file = file.replace(regex, fs.readFileSync(paths[partial]).toString());
} else {
winston.warn('[meta/templates] Partial not loaded: ' + matches[1]);
file = file.replace(regex, "");
}
preparePaths(baseTemplatesPaths, function(err, paths) {
if (err) {
return callback(err);
}
async.each(Object.keys(paths), function(relativePath, next) {
var file = fs.readFileSync(paths[relativePath]).toString(),
matches = null,
regex = /[ \t]*<!-- IMPORT ([\s\S]*?)? -->[ \t]*/;
while((matches = file.match(regex)) !== null) {
var partial = "/" + matches[1];
if (paths[partial] && relativePath !== partial) {
file = file.replace(regex, fs.readFileSync(paths[partial]).toString());
} else {
winston.warn('[meta/templates] Partial not loaded: ' + matches[1]);
file = file.replace(regex, "");
}
}
if (relativePath.match(/^\/admin\/[\s\S]*?/)) {
addIndex(relativePath, file);
if (relativePath.match(/^\/admin\/[\s\S]*?/)) {
addIndex(relativePath, file);
}
mkdirp.sync(path.join(viewsPath, relativePath.split('/').slice(0, -1).join('/')));
fs.writeFile(path.join(viewsPath, relativePath), file, next);
}, function(err) {
if (err) {
winston.error('[meta/templates] ' + err.stack);
return callback(err);
}
compileIndex(viewsPath, function() {
winston.verbose('[meta/templates] Successfully compiled templates.');
emitter.emit('templates:compiled');
if (process.send) {
process.send({
action: 'templates:compiled'
});
}
mkdirp.sync(path.join(viewsPath, relativePath.split('/').slice(0, -1).join('/')));
fs.writeFile(path.join(viewsPath, relativePath), file, next);
}, function(err) {
if (err) {
winston.error('[meta/templates] ' + err.stack);
return callback(err);
}
compileIndex(viewsPath, function() {
winston.verbose('[meta/templates] Successfully compiled templates.');
emitter.emit('templates:compiled');
if (process.send) {
process.send({
action: 'templates:compiled'
});
}
callback();
});
callback();
});
});
});
};
}
var searchIndex = {};
function addIndex(path, file) {
searchIndex[path] = file;

View File

@@ -48,7 +48,11 @@ middleware.applyCSRF = csrf();
middleware.ensureLoggedIn = ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login');
middleware.pageView = function(req, res, next) {
analytics.pageView(req.ip);
analytics.pageView({
ip: req.ip,
path: req.path,
uid: req.hasOwnProperty('user') && req.user.hasOwnProperty('uid') ? parseInt(req.user.uid, 10) : 0
});
plugins.fireHook('action:middleware.pageView', {req: req});

View File

@@ -44,9 +44,6 @@ var async = require('async'),
return next(null, null);
}
if (notification.bodyShort) {
notification.bodyShort = S(notification.bodyShort).escapeHTML().s;
}
if (notification.bodyLong) {
notification.bodyLong = S(notification.bodyLong).escapeHTML().s;
}
@@ -58,6 +55,11 @@ var async = require('async'),
}
notification.image = userData.picture || null;
notification.user = userData;
if (userData.username === '[[global:guest]]') {
notification.bodyShort = notification.bodyShort.replace(/([\s\S]*?),[\s\S]*?,([\s\S]*?)/, '$1, [[global:guest]], $2');
}
next(null, notification);
});
return;
@@ -388,9 +390,9 @@ var async = require('async'),
var numUsers = usernames.length;
if (numUsers === 2) {
notifications[modifyIndex].bodyShort = '[[' + mergeId + '_dual, ' + usernames.join(', ') + ', ' + notifications[modifyIndex].topicTitle + ']]'
notifications[modifyIndex].bodyShort = '[[' + mergeId + '_dual, ' + usernames.join(', ') + ', ' + notifications[modifyIndex].topicTitle + ']]';
} else if (numUsers > 2) {
notifications[modifyIndex].bodyShort = '[[' + mergeId + '_multiple, ' + usernames[0] + ', ' + (numUsers-1) + ', ' + notifications[modifyIndex].topicTitle + ']]'
notifications[modifyIndex].bodyShort = '[[' + mergeId + '_multiple, ' + usernames[0] + ', ' + (numUsers-1) + ', ' + notifications[modifyIndex].topicTitle + ']]';
}
break;
}

View File

@@ -1,31 +1,32 @@
'use strict';
var async = require('async'),
_ = require('underscore'),
var async = require('async');
var _ = require('underscore');
db = require('../database'),
topics = require('../topics'),
user = require('../user'),
plugins = require('../plugins');
var db = require('../database');
var topics = require('../topics');
var user = require('../user');
var plugins = require('../plugins');
module.exports = function(Posts) {
Posts.delete = function(pid, callback) {
Posts.delete = function(pid, uid, callback) {
var postData;
async.waterfall([
function(next) {
function (next) {
plugins.fireHook('filter:post.delete', {pid: pid, uid: uid}, next);
},
function (data, next) {
Posts.setPostField(pid, 'deleted', 1, next);
},
function(next) {
function (next) {
Posts.getPostFields(pid, ['pid', 'tid', 'uid', 'timestamp'], next);
},
function(_post, next) {
function (_post, next) {
postData = _post;
topics.getTopicField(_post.tid, 'cid', next);
},
function(cid, next) {
plugins.fireHook('action:post.delete', pid);
function (cid, next) {
async.parallel([
function(next) {
updateTopicTimestamp(postData.tid, next);
@@ -40,29 +41,31 @@ module.exports = function(Posts) {
topics.updateTeaser(postData.tid, next);
}
], function(err) {
plugins.fireHook('action:post.delete', pid);
next(err, postData);
});
}
], callback);
};
Posts.restore = function(pid, callback) {
Posts.restore = function(pid, uid, callback) {
var postData;
async.waterfall([
function(next) {
function (next) {
plugins.fireHook('filter:post.restore', {pid: pid, uid: uid}, next);
},
function (data, next) {
Posts.setPostField(pid, 'deleted', 0, next);
},
function(next) {
function (next) {
Posts.getPostFields(pid, ['pid', 'tid', 'uid', 'content', 'timestamp'], next);
},
function(_post, next) {
function (_post, next) {
postData = _post;
topics.getTopicField(_post.tid, 'cid', next);
},
function(cid, next) {
function (cid, next) {
postData.cid = cid;
plugins.fireHook('action:post.restore', _.clone(postData));
async.parallel([
function(next) {
updateTopicTimestamp(postData.tid, next);
@@ -74,6 +77,7 @@ module.exports = function(Posts) {
topics.updateTeaser(postData.tid, next);
}
], function(err) {
plugins.fireHook('action:post.restore', _.clone(postData));
next(err, postData);
});
}
@@ -99,40 +103,46 @@ module.exports = function(Posts) {
});
}
Posts.purge = function(pid, callback) {
Posts.exists(pid, function(err, exists) {
if (err || !exists) {
return callback(err);
Posts.purge = function(pid, uid, callback) {
async.waterfall([
function (next) {
Posts.exists(pid, next);
},
function (exists, next) {
if (!exists) {
return callback();
}
plugins.fireHook('filter:post.purge', {pid: pid, uid: uid}, next);
},
function (data, next) {
async.parallel([
function (next) {
deletePostFromTopicAndUser(pid, next);
},
function (next) {
deletePostFromCategoryRecentPosts(pid, next);
},
function (next) {
deletePostFromUsersFavourites(pid, next);
},
function (next) {
deletePostFromUsersVotes(pid, next);
},
function (next) {
db.sortedSetsRemove(['posts:pid', 'posts:flagged'], pid, next);
},
function (next) {
Posts.dismissFlag(pid, next);
}
], function(err) {
if (err) {
return next(err);
}
plugins.fireHook('action:post.purge', pid);
db.delete('post:' + pid, next);
});
}
async.parallel([
function(next) {
deletePostFromTopicAndUser(pid, next);
},
function(next) {
deletePostFromCategoryRecentPosts(pid, next);
},
function(next) {
deletePostFromUsersFavourites(pid, next);
},
function(next) {
deletePostFromUsersVotes(pid, next);
},
function(next) {
db.sortedSetsRemove(['posts:pid', 'posts:flagged'], pid, next);
},
function(next) {
Posts.dismissFlag(pid, next);
}
], function(err) {
if (err) {
return callback(err);
}
plugins.fireHook('action:post.purge', pid);
db.delete('post:' + pid, callback);
});
});
], callback);
};
function deletePostFromTopicAndUser(pid, callback) {

View File

@@ -1,8 +1,8 @@
'use strict';
var cache = require('./cache'),
plugins = require('../plugins');
var cache = require('./cache');
var plugins = require('../plugins');
module.exports = function(Posts) {

View File

@@ -1,9 +1,9 @@
'use strict';
var async = require('async'),
var async = require('async');
privileges = require('../privileges'),
cache = require('./cache');
var privileges = require('../privileges');
var cache = require('./cache');
module.exports = function(Posts) {
Posts.tools = {};
@@ -40,25 +40,20 @@ module.exports = function(Posts) {
if (!canEdit) {
return next(new Error('[[error:no-privileges]]'));
}
next();
}
], function (err) {
if (err) {
return callback(err);
}
if (isDelete) {
cache.del(pid);
Posts.delete(pid, callback);
} else {
Posts.restore(pid, function(err, postData) {
if (err) {
return callback(err);
}
Posts.parsePost(postData, callback);
});
if (isDelete) {
cache.del(pid);
Posts.delete(pid, uid, next);
} else {
Posts.restore(pid, uid, function(err, postData) {
if (err) {
return next(err);
}
Posts.parsePost(postData, next);
});
}
}
});
], callback);
}
Posts.tools.purge = function(uid, pid, callback) {
@@ -71,7 +66,7 @@ module.exports = function(Posts) {
return next(new Error('[[error:no-privileges]]'));
}
cache.del(pid);
Posts.purge(pid, next);
Posts.purge(pid, uid, next);
}
], callback);
};

View File

@@ -15,6 +15,7 @@ function apiRoutes(router, middleware, controllers) {
router.post('/uploadfavicon', middlewares, controllers.admin.uploads.uploadFavicon);
router.post('/uploadTouchIcon', middlewares, controllers.admin.uploads.uploadTouchIcon);
router.post('/uploadlogo', middlewares, controllers.admin.uploads.uploadLogo);
router.post('/upload/sound', middlewares, controllers.admin.uploads.uploadSound);
router.post('/uploadDefaultAvatar', middlewares, controllers.admin.uploads.uploadDefaultAvatar);
}
@@ -47,6 +48,7 @@ function addRoutes(router, middleware, controllers) {
router.get('/general/sounds', middlewares, controllers.admin.sounds.get);
router.get('/general/navigation', middlewares, controllers.admin.navigation.get);
router.get('/general/homepage', middlewares, controllers.admin.homepage.get);
router.get('/general/social', middlewares, controllers.admin.social.get);
router.get('/manage/categories', middlewares, controllers.admin.categories.getAll);
router.get('/manage/categories/:category_id', middlewares, controllers.admin.categories.get);

View File

@@ -173,11 +173,11 @@ function handle404(app, middleware) {
res.status(404);
if (res.locals.isAPI) {
return res.json({path: req.path.replace(/^\/api/, ''), title: '[[global:404.title]]'});
return res.json({path: validator.escape(req.path.replace(/^\/api/, '') || ''), title: '[[global:404.title]]'});
}
middleware.buildHeader(req, res, function() {
res.render('404', {path: req.path, title: '[[global:404.title]]'});
res.render('404', {path: validator.escape(req.path || ''), title: '[[global:404.title]]'});
});
} else {
res.status(404).type('txt').send('Not found');
@@ -201,10 +201,10 @@ function handleErrors(app, middleware) {
res.status(err.status || 500);
if (res.locals.isAPI) {
res.json({path: req.path, error: err.message});
res.json({path: validator.escape(req.path || ''), error: err.message});
} else {
middleware.buildHeader(req, res, function() {
res.render('500', {path: req.path, error: validator.escape(err.message)});
res.render('500', {path: validator.escape(req.path || ''), error: validator.escape(err.message)});
});
}
});

58
src/social.js Normal file
View File

@@ -0,0 +1,58 @@
"use strict";
var plugins = require('./plugins');
var db = require('./database');
var async = require('async');
var social = {};
social.getPostSharing = function(callback) {
var networks = [
{
id: "facebook",
name: "Facebook",
class: "fa-facebook"
},
{
id: "twitter",
name: "Twitter",
class: "fa-twitter"
},
{
id: "google",
name: "Google+",
class: "fa-google-plus"
}
];
async.waterfall([
function(next) {
plugins.fireHook('filter:social.posts', networks, next);
},
function(networks, next) {
db.getSetMembers('social:posts.activated', function(err, activated) {
if (err) {
return next(err);
}
networks.forEach(function(network, i) {
networks[i].activated = (activated.indexOf(network.id) !== -1);
});
next(null, networks);
});
}
], callback);
};
social.setActivePostSharingNetworks = function(networkIDs, callback) {
db.delete('social:posts.activated', function(err) {
if (!networkIDs.length) {
return callback(err);
}
db.setAdd('social:posts.activated', networkIDs, callback);
});
};
module.exports = social;

View File

@@ -25,6 +25,7 @@ var async = require('async'),
rewards: require('./admin/rewards'),
navigation: require('./admin/navigation'),
rooms: require('./admin/rooms'),
social: require('./admin/social'),
themes: {},
plugins: {},
widgets: {},
@@ -216,16 +217,16 @@ SocketAdmin.analytics.get = function(socket, data, callback) {
async.parallel({
uniqueVisitors: function(next) {
if (data.units === 'days') {
getDailyStatsForSet('analytics:uniquevisitors', data.until || Date.now(), data.amount, next);
analytics.getDailyStatsForSet('analytics:uniquevisitors', data.until || Date.now(), data.amount, next);
} else {
getHourlyStatsForSet('analytics:uniquevisitors', data.until || Date.now(), data.amount, next);
analytics.getHourlyStatsForSet('analytics:uniquevisitors', data.until || Date.now(), data.amount, next);
}
},
pageviews: function(next) {
if (data.units === 'days') {
getDailyStatsForSet('analytics:pageviews', data.until || Date.now(), data.amount, next);
analytics.getDailyStatsForSet('analytics:pageviews', data.until || Date.now(), data.amount, next);
} else {
getHourlyStatsForSet('analytics:pageviews', data.until || Date.now(), data.amount, next);
analytics.getHourlyStatsForSet('analytics:pageviews', data.until || Date.now(), data.amount, next);
}
},
monthlyPageViews: function(next) {
@@ -250,62 +251,6 @@ SocketAdmin.logs.clear = function(socket, data, callback) {
meta.logs.clear(callback);
};
function getHourlyStatsForSet(set, hour, numHours, callback) {
var terms = {},
hoursArr = [];
hour = new Date(hour);
hour.setHours(hour.getHours(), 0, 0, 0);
for (var i = 0, ii = numHours; i < ii; i++) {
hoursArr.push(hour.getTime());
hour.setHours(hour.getHours() - 1, 0, 0, 0);
}
db.sortedSetScores(set, hoursArr, function(err, counts) {
if (err) {
return callback(err);
}
hoursArr.forEach(function(term, index) {
terms[term] = parseInt(counts[index], 10) || 0;
});
var termsArr = [];
hoursArr.reverse();
hoursArr.forEach(function(hour) {
termsArr.push(terms[hour]);
});
callback(null, termsArr);
});
}
function getDailyStatsForSet(set, day, numDays, callback) {
var daysArr = [];
day = new Date(day);
day.setHours(0, 0, 0, 0);
async.whilst(function() {
return numDays--;
}, function(next) {
getHourlyStatsForSet(set, day.getTime()-(1000*60*60*24*numDays), 24, function(err, day) {
if (err) {
return next(err);
}
daysArr.push(day.reduce(function(cur, next) {
return cur+next;
}));
next();
});
}, function(err) {
callback(err, daysArr);
});
}
SocketAdmin.getMoreEvents = function(socket, next, callback) {
var start = parseInt(next, 10);
if (start < 0) {

View File

@@ -42,7 +42,7 @@ Categories.getNames = function(socket, data, callback) {
};
Categories.purge = function(socket, cid, callback) {
categories.purge(cid, callback);
categories.purge(cid, socket.uid, callback);
};
Categories.update = function(socket, data, callback) {

View File

@@ -0,0 +1,10 @@
"use strict";
var social = require('../../social'),
SocketSocial = {};
SocketSocial.savePostSharingNetworks = function(socket, data, callback) {
social.setActivePostSharingNetworks(data, callback);
};
module.exports = SocketSocial;

View File

@@ -3,7 +3,6 @@
var async = require('async');
var winston = require('winston');
var nconf = require('nconf');
var validator = require('validator');
var websockets = require('./index');
var user = require('../user');
@@ -16,13 +15,12 @@ var plugins = require('../plugins');
var SocketHelpers = {};
SocketHelpers.notifyOnlineUsers = function(uid, result) {
var cid = result.posts[0].topic.cid;
async.waterfall([
function(next) {
user.getUidsFromSet('users:online', 0, -1, next);
},
function(uids, next) {
privileges.categories.filterUids('read', cid, uids, next);
privileges.topics.filterUids('read', result.posts[0].topic.tid, uids, next);
},
function(uids, next) {
plugins.fireHook('filter:sockets.sendNewPostToUids', {uidsTo: uids, uidFrom: uid, type: 'newPost'}, next);

View File

@@ -6,15 +6,20 @@ var SocketIO = require('socket.io'),
nconf = require('nconf'),
cookieParser = require('cookie-parser')(nconf.get('secret')),
winston = require('winston'),
var SocketIO = require('socket.io');
var socketioWildcard = require('socketio-wildcard')();
var async = require('async');
var nconf = require('nconf');
var cookieParser = require('cookie-parser')(nconf.get('secret'));
var winston = require('winston');
db = require('../database'),
user = require('../user'),
logger = require('../logger'),
ratelimit = require('../middleware/ratelimit'),
cls = require('../middleware/cls'),
var db = require('../database');
var user = require('../user');
var logger = require('../logger');
var ratelimit = require('../middleware/ratelimit');
Sockets = {},
Namespaces = {};
var Sockets = {};
var Namespaces = {};
var io;
@@ -102,15 +107,15 @@ function onMessage(socket, payload) {
return winston.warn('[socket.io] Empty method name');
}
var parts = eventName.toString().split('.'),
namespace = parts[0],
methodToCall = parts.reduce(function(prev, cur) {
if (prev !== null && prev[cur]) {
return prev[cur];
} else {
return null;
}
}, Namespaces);
var parts = eventName.toString().split('.');
var namespace = parts[0];
var methodToCall = parts.reduce(function(prev, cur) {
if (prev !== null && prev[cur]) {
return prev[cur];
} else {
return null;
}
}, Namespaces);
if(!methodToCall) {
if (process.env.NODE_ENV === 'development') {
@@ -130,16 +135,23 @@ function onMessage(socket, payload) {
return socket.disconnect();
}
if (Namespaces[namespace].before) {
Namespaces[namespace].before(socket, eventName, params, function(err) {
if (err) {
return callback({message: err.message});
async.waterfall([
function (next) {
validateSession(socket, next);
},
function (next) {
if (Namespaces[namespace].before) {
Namespaces[namespace].before(socket, eventName, params, next);
} else {
next();
}
callMethod(methodToCall, socket, params, callback);
});
} else {
callMethod(methodToCall, socket, params, callback);
}
},
function (next) {
methodToCall(socket, params, next);
}
], function(err, result) {
callback(err ? {message: err.message} : null, result);
});
}
function requireModules() {
@@ -152,19 +164,33 @@ function requireModules() {
});
}
function authorize(socket, callback) {
var handshake = socket.request;
function validateSession(socket, callback) {
var req = socket.request;
if (!req.signedCookies || !req.signedCookies['express.sid']) {
return callback(new Error('[[error:invalid-session]]'));
}
db.sessionStore.get(req.signedCookies['express.sid'], function(err, sessionData) {
if (err || !sessionData) {
return callback(err || new Error('[[error:invalid-session]]'));
}
if (!handshake) {
callback();
});
}
function authorize(socket, callback) {
var request = socket.request;
if (!request) {
return callback(new Error('[[error:not-authorized]]'));
}
async.waterfall([
function(next) {
cookieParser(handshake, {}, next);
cookieParser(request, {}, next);
},
function(next) {
db.sessionStore.get(handshake.signedCookies['express.sid'], function(err, sessionData) {
db.sessionStore.get(request.signedCookies['express.sid'], function(err, sessionData) {
if (err) {
return next(err);
}
@@ -192,12 +218,6 @@ function addRedisAdapter(io) {
}
}
function callMethod(method, socket, params, callback) {
method(socket, params, function(err, result) {
callback(err ? {message: err.message} : null, result);
});
}
Sockets.in = function(room) {
return io.in(room);
};
@@ -235,9 +255,9 @@ Sockets.getOnlineAnonCount = function () {
};
Sockets.reqFromSocket = function(socket) {
var headers = socket.request.headers,
host = headers.host,
referer = headers.referer || '';
var headers = socket.request.headers;
var host = headers.host;
var referer = headers.referer || '';
return {
ip: headers['x-forwarded-for'] || socket.ip,

View File

@@ -266,31 +266,6 @@ SocketModules.chats.renameRoom = function(socket, data, callback) {
], callback);
};
SocketModules.chats.userStartTyping = function(socket, data, callback) {
sendTypingNotification('event:chats.userStartTyping', socket, data, callback);
};
SocketModules.chats.userStopTyping = function(socket, data, callback) {
sendTypingNotification('event:chats.userStopTyping', socket, data, callback);
};
function sendTypingNotification(event, socket, data, callback) {
if (!socket.uid || !data || !data.roomId) {
return;
}
Messaging.getUidsInRoom(data.roomId, 0, -1, function(err, uids) {
if (err) {
return callback(err);
}
uids.forEach(function(uid) {
if (socket.uid !== parseInt(uid, 10)) {
server.in('uid_' + uid).emit(event, data.fromUid);
}
});
});
}
SocketModules.chats.getRecentChats = function(socket, data, callback) {
if (!data || !utils.isNumber(data.after)) {
return callback(new Error('[[error:invalid-data]]'));

View File

@@ -8,6 +8,7 @@ var websockets = require('../index');
var socketTopics = require('../topics');
var privileges = require('../../privileges');
var plugins = require('../../plugins');
var social = require('../../social');
var favourites = require('../../favourites');
module.exports = function(SocketPosts) {
@@ -29,6 +30,9 @@ module.exports = function(SocketPosts) {
},
tools: function(next) {
plugins.fireHook('filter:post.tools', {pid: data.pid, uid: socket.uid, tools: []}, next);
},
postSharing: function(next) {
social.getPostSharing(next);
}
}, function(err, results) {
if (err) {

View File

@@ -2,6 +2,7 @@
var async = require('async');
var winston = require('winston');
var path = require('path');
var user = require('../../user');
var plugins = require('../../plugins');
@@ -73,20 +74,21 @@ module.exports = function(SocketUser) {
user.isAdminOrSelf(socket.uid, data.uid, next);
},
function (next) {
user.getUserField(data.uid, 'uploadedpicture', next);
user.getUserFields(data.uid, ['uploadedpicture', 'picture'], next);
},
function(uploadedPicture, next) {
if (!uploadedPicture.startsWith('http')) {
require('fs').unlink(uploadedPicture, function(err) {
function(userData, next) {
if (!userData.uploadedpicture.startsWith('http')) {
require('fs').unlink(path.join(__dirname, '../../../public', userData.uploadedpicture), function(err) {
if (err) {
winston.error(err);
}
});
}
user.setUserField(data.uid, 'uploadedpicture', '', next);
},
function(next) {
user.getUserField(data.uid, 'picture', next);
user.setUserFields(data.uid, {
uploadedpicture: '',
picture: userData.uploadedpicture === userData.picture ? '' : userData.picture // if current picture is uploaded picture, reset to user icon
}, next);
}
], callback);
};

View File

@@ -19,6 +19,7 @@ module.exports = function(SocketUser) {
searchBy: data.searchBy,
sortBy: data.sortBy,
onlineOnly: data.onlineOnly,
bannedOnly: data.bannedOnly,
uid: socket.uid
}, function(err, result) {
if (err) {

View File

@@ -6,6 +6,7 @@ var async = require('async'),
db = require('../database'),
utils = require('../../public/src/utils'),
plugins = require('../plugins'),
analytics = require('../analytics'),
user = require('../user'),
meta = require('../meta'),
posts = require('../posts'),
@@ -15,7 +16,7 @@ var async = require('async'),
module.exports = function(Topics) {
Topics.create = function(data, callback) {
// This is an interal method, consider using Topics.post instead
// This is an internal method, consider using Topics.post instead
var timestamp = data.timestamp || Date.now();
var topicData;
@@ -171,6 +172,7 @@ module.exports = function(Topics) {
data.topicData.mainPost = data.postData;
data.postData.index = 0;
analytics.increment(['topics', 'topics:byCid:' + data.topicData.cid]);
plugins.fireHook('action:topic.post', data.topicData);
if (parseInt(uid, 10)) {
@@ -186,12 +188,12 @@ module.exports = function(Topics) {
};
Topics.reply = function(data, callback) {
var tid = data.tid,
uid = data.uid,
content = data.content,
postData;
var tid = data.tid;
var uid = data.uid;
var content = data.content;
var postData;
var cid;
async.waterfall([
function(next) {
Topics.getTopicField(tid, 'cid', next);
@@ -256,6 +258,7 @@ module.exports = function(Topics) {
}
Topics.notifyFollowers(postData, uid);
analytics.increment(['posts', 'posts:byCid:' + cid]);
plugins.fireHook('action:topic.reply', postData);
next(null, postData);

View File

@@ -58,6 +58,7 @@ module.exports = function(Topics) {
if (!topic) {
return;
}
topic.titleRaw = topic.title;
topic.title = validator.escape(topic.title);
topic.relativeTime = utils.toISOString(topic.timestamp);
topic.lastposttimeISO = utils.toISOString(topic.lastposttime);

View File

@@ -85,7 +85,7 @@ module.exports = function(Topics) {
});
};
Topics.purgePostsAndTopic = function(tid, callback) {
Topics.purgePostsAndTopic = function(tid, uid, callback) {
var mainPid;
async.waterfall([
function (next) {
@@ -94,11 +94,13 @@ module.exports = function(Topics) {
function (_mainPid, next) {
mainPid = _mainPid;
batch.processSortedSet('tid:' + tid + ':posts', function(pids, next) {
async.eachLimit(pids, 10, posts.purge, next);
async.eachLimit(pids, 10, function(pid, next) {
posts.purge(pid, uid, next);
}, next);
}, {alwaysStartAt: 0}, next);
},
function (next) {
posts.purge(mainPid, next);
posts.purge(mainPid, uid, next);
},
function (next) {
Topics.purge(tid, next);

View File

@@ -15,7 +15,7 @@ var async = require('async'),
module.exports = function(Topics) {
Topics.onNewPostMade = function(postData, callback) {
async.parallel([
async.series([
function(next) {
Topics.increasePostCount(postData.tid, next);
},
@@ -143,6 +143,7 @@ module.exports = function(Topics) {
if (post) {
post.display_moderator_tools = topicPrivileges.isAdminOrMod || post.selfPost;
post.display_move_tools = topicPrivileges.isAdminOrMod && post.index !== 0;
post.display_post_menu = topicPrivileges.isAdminOrMod || post.selfPost || !post.deleted;
if (post.deleted && !(topicPrivileges.isAdminOrMod || post.selfPost)) {
post.content = '[[topic:post_is_deleted]]';
}

View File

@@ -2,9 +2,8 @@
'use strict';
var async = require('async'),
winston = require('winston'),
db = require('../database');
var async = require('async');
var db = require('../database');
module.exports = function(Topics) {
var terms = {
@@ -42,12 +41,22 @@ module.exports = function(Topics) {
Topics.updateTimestamp = function(tid, timestamp, callback) {
async.parallel([
function(next) {
Topics.updateRecent(tid, timestamp, next);
async.waterfall([
function (next) {
Topics.getTopicField(tid, 'deleted', next);
},
function (deleted, next) {
if (parseInt(deleted, 10) === 1) {
return next();
}
Topics.updateRecent(tid, timestamp, next);
}
], next);
},
function(next) {
Topics.setTopicField(tid, 'lastposttime', timestamp, next);
}
], function(err, results) {
], function(err) {
callback(err);
});
};

View File

@@ -24,8 +24,8 @@ module.exports = function(Topics) {
},
function (data, next) {
tags = data.tags.slice(0, meta.config.maximumTagsPerTopic || 5);
tags = tags.map(Topics.cleanUpTag).filter(function(tag) {
return tag && tag.length >= (meta.config.minimumTagLength || 3);
tags = tags.map(Topics.cleanUpTag).filter(function(tag, index, array) {
return tag && tag.length >= (meta.config.minimumTagLength || 3) && array.indexOf(tag) === index;
});
var keys = tags.map(function(tag) {

View File

@@ -94,7 +94,7 @@ module.exports = function(Topics) {
function (_cid, next) {
cid = _cid;
Topics.purgePostsAndTopic(tid, next);
Topics.purgePostsAndTopic(tid, uid, next);
},
function (next) {
next(null, {tid: tid, cid: cid, uid: uid});

View File

@@ -15,7 +15,7 @@ var utils = require('../../public/src/utils');
module.exports = function(Topics) {
Topics.getTotalUnread = function(uid, callback) {
Topics.getUnreadTids(0, uid, 0, 20, function(err, tids) {
Topics.getUnreadTids(0, uid, 0, 99, function(err, tids) {
callback(err, tids ? tids.length : 0);
});
};

View File

@@ -10,7 +10,7 @@ var db = require('./database'),
schemaDate, thisSchemaDate,
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema
latestSchema = Date.UTC(2016, 0, 23);
latestSchema = Date.UTC(2016, 1, 25);
Upgrade.check = function(callback) {
db.get('schemaDate', function(err, value) {
@@ -410,6 +410,34 @@ Upgrade.upgrade = function(callback) {
winston.info('[2016/01/23] Creating Global moderators group skipped!');
next();
}
},
function(next) {
thisSchemaDate = Date.UTC(2016, 1, 25);
if (schemaDate < thisSchemaDate) {
updatesMade = true;
winston.info('[2016/02/25] Social: Post Sharing');
var social = require('./social');
async.parallel([
function (next) {
social.setActivePostSharingNetworks(['facebook', 'google', 'twitter'], next);
},
function (next) {
db.deleteObjectField('config', 'disableSocialButtons', next);
}
], function(err) {
if (err) {
return next(err);
}
winston.info('[2016/02/25] Social: Post Sharing done!');
Upgrade.update(thisSchemaDate, next);
});
} else {
winston.info('[2016/02/25] Social: Post Sharing skipped!');
next();
}
}
// Add new schema updates here
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!!

View File

@@ -70,7 +70,7 @@ module.exports = function(User) {
}
async.waterfall([
async.apply(db.getSortedSetRange, 'uid:' + uid + ':sessions', 0, -1),
async.apply(db.getSortedSetRevRange, 'uid:' + uid + ':sessions', 0, -1),
function (sids, next) {
_sids = sids;
async.map(sids, db.sessionStore.get.bind(db.sessionStore), next);

View File

@@ -123,7 +123,7 @@ var async = require('async'),
return null;
}
notification.path = pidToPaths[notification.pid] || notification.path || '';
notification.path = pidToPaths[notification.pid] || notification.path || null;
if (notification.nid.startsWith('follow')) {
notification.path = '/user/' + notification.user.userslug;
@@ -131,6 +131,9 @@ var async = require('async'),
notification.datetimeISO = utils.toISOString(notification.datetime);
return notification;
}).filter(function(notification) {
// Remove notifications that do not resolve to a path
return notification && notification.path !== null;
});
callback(null, notifications);
@@ -157,7 +160,7 @@ var async = require('async'),
posts.getPostIndices(postData, uid, next);
},
topics: function(next) {
db.getObjectsFields(topicKeys, ['slug'], next);
db.getObjectsFields(topicKeys, ['slug', 'deleted'], next);
}
}, function(err, results) {
if (err) {
@@ -166,6 +169,11 @@ var async = require('async'),
var pidToPaths = {};
pids.forEach(function(pid, index) {
if (parseInt(results.topics[index].deleted, 10) === 1) {
pidToPaths[pid] = null;
return;
}
var slug = results.topics[index] ? results.topics[index].slug : null;
var postIndex = utils.isNumber(results.indices[index]) ? parseInt(results.indices[index], 10) + 1 : null;
@@ -200,7 +208,7 @@ var async = require('async'),
if (!parseInt(uid, 10)) {
return callback(null, 0);
}
db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 20, function(err, nids) {
db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function(err, nids) {
callback(err, Array.isArray(nids) ? nids.length : 0);
});
};

View File

@@ -19,17 +19,21 @@ module.exports = function(User) {
User.isPasswordCorrect = function(uid, password, callback) {
password = password || '';
async.waterfall([
function (next) {
User.isPasswordValid(password, next);
},
function (next) {
db.getObjectField('user:' + uid, 'password', next);
},
function (hashedPassword, next) {
if (!hashedPassword) {
return callback();
return callback(null, true);
}
Password.compare(password, hashedPassword, next);
User.isPasswordValid(password, function(err) {
if (err) {
return next(err);
}
Password.compare(password, hashedPassword, next);
});
}
], callback);
};

View File

@@ -63,7 +63,7 @@ module.exports = function(User) {
};
User.onNewPostMade = function(postData, callback) {
async.parallel([
async.series([
function(next) {
User.addPostIdToUser(postData.uid, postData.pid, postData.timestamp, next);
},

View File

@@ -74,10 +74,6 @@ module.exports = function(User) {
var userslug = utils.slugify(data.username);
if (userslug === userData.userslug) {
return next();
}
if (data.username.length < meta.config.minimumUsernameLength) {
return next(new Error('[[error:username-too-short]]'));
}
@@ -90,6 +86,10 @@ module.exports = function(User) {
return next(new Error('[[error:invalid-username]]'));
}
if (userslug === userData.userslug) {
return next();
}
User.existsBySlug(userslug, function(err, exists) {
if (err) {
return next(err);
@@ -110,7 +110,7 @@ module.exports = function(User) {
return callback(err);
}
plugins.fireHook('action:user.updateProfile', {data: data, uid: uid});
User.getUserFields(uid, ['email', 'username', 'userslug', 'picture'], callback);
User.getUserFields(uid, ['email', 'username', 'userslug', 'picture', 'icon:text', 'icon:bgColor'], callback);
});
});

View File

@@ -84,7 +84,7 @@ module.exports = function(User) {
function filterAndSortUids(uids, data, callback) {
var sortBy = data.sortBy || 'joindate';
var fields = ['uid', 'status', 'lastonline', sortBy];
var fields = ['uid', 'status', 'lastonline', 'banned', sortBy];
User.getUsersFields(uids, fields, function(err, userData) {
if (err) {
@@ -96,6 +96,12 @@ module.exports = function(User) {
return user && user.status !== 'offline' && (Date.now() - parseInt(user.lastonline, 10) < 300000);
});
}
if(data.bannedOnly) {
userData = userData.filter(function(user) {
return user && user.banned;
});
}
sortUsers(userData, sortBy);

View File

@@ -58,14 +58,14 @@ module.exports = function(User) {
var defaultTopicsPerPage = parseInt(meta.config.topicsPerPage, 10) || 20;
var defaultPostsPerPage = parseInt(meta.config.postsPerPage, 10) || 20;
settings.showemail = parseInt(settings.showemail, 10) === 1;
settings.showfullname = parseInt(settings.showfullname, 10) === 1;
settings.showemail = parseInt(getSetting(settings, 'showemail', 0), 10) === 1;
settings.showfullname = parseInt(getSetting(settings, 'showfullname', 0), 10) === 1;
settings.openOutgoingLinksInNewTab = parseInt(getSetting(settings, 'openOutgoingLinksInNewTab', 0), 10) === 1;
settings.dailyDigestFreq = getSetting(settings, 'dailyDigestFreq', 'off');
settings.usePagination = parseInt(getSetting(settings, 'usePagination', 0), 10) === 1;
settings.topicsPerPage = Math.min(settings.topicsPerPage ? parseInt(settings.topicsPerPage, 10) : defaultTopicsPerPage, defaultTopicsPerPage);
settings.postsPerPage = Math.min(settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : defaultPostsPerPage, defaultPostsPerPage);
settings.notificationSounds = parseInt(settings.notificationSounds, 10) === 1;
settings.notificationSounds = parseInt(getSetting(settings, 'notificationSounds', 0), 10) === 1;
settings.userLang = settings.userLang || meta.config.defaultLang || 'en_GB';
settings.topicPostSort = getSetting(settings, 'topicPostSort', 'oldest_to_newest');
settings.categoryTopicSort = getSetting(settings, 'categoryTopicSort', 'newest_to_oldest');
@@ -73,7 +73,7 @@ module.exports = function(User) {
settings.followTopicsOnReply = parseInt(getSetting(settings, 'followTopicsOnReply', 0), 10) === 1;
settings.sendChatNotifications = parseInt(getSetting(settings, 'sendChatNotifications', 0), 10) === 1;
settings.sendPostNotifications = parseInt(getSetting(settings, 'sendPostNotifications', 0), 10) === 1;
settings.restrictChat = parseInt(settings.restrictChat, 10) === 1;
settings.restrictChat = parseInt(getSetting(settings, 'restrictChat', 0), 10) === 1;
settings.topicSearchEnabled = parseInt(getSetting(settings, 'topicSearchEnabled', 0), 10) === 1;
settings.bootswatchSkin = settings.bootswatchSkin || 'default';
@@ -91,8 +91,12 @@ module.exports = function(User) {
}
User.saveSettings = function(uid, data, callback) {
if (invalidPaginationSettings(data)) {
return callback(new Error('[[error:invalid-pagination-value]]'));
if (!data.postsPerPage || parseInt(data.postsPerPage, 10) <= 1 || parseInt(data.postsPerPage, 10) > meta.config.postsPerPage) {
return callback(new Error('[[error:invalid-pagination-value, 2, ' + meta.config.postsPerPage + ']]'));
}
if (!data.topicsPerPage || parseInt(data.topicsPerPage, 10) <= 1 || parseInt(data.topicsPerPage, 10) > meta.config.topicsPerPage) {
return callback(new Error('[[error:invalid-pagination-value, 2, ' + meta.config.topicsPerPage + ']]'));
}
data.userLang = data.userLang || meta.config.defaultLang;
@@ -136,12 +140,6 @@ module.exports = function(User) {
], callback);
};
function invalidPaginationSettings(data) {
return !data.topicsPerPage || !data.postsPerPage ||
parseInt(data.topicsPerPage, 10) <= 0 || parseInt(data.postsPerPage, 10) <= 0 ||
parseInt(data.topicsPerPage, 10) > meta.config.topicsPerPage || parseInt(data.postsPerPage, 10) > meta.config.postsPerPage;
}
function updateDigestSetting(uid, dailyDigestFreq, callback) {
async.waterfall([
function(next) {

View File

@@ -1,6 +1,7 @@
<ul class="nav nav-pills">
<li class="active"><a href="#installed" data-toggle="tab">Installed Plugins</a></li>
<li><a href="#download" data-toggle="tab">Download Plugins</a></li>
<li><a href="#upgrade" data-toggle="tab">Upgradable Plugins</a></li>
</ul>
<br />
@@ -15,13 +16,14 @@
</ul>
</div>
<div class="tab-pane fade" id="download">
<div class="panel-body">
<ul class="download">
<!-- BEGIN download -->
<!-- IMPORT admin/partials/download_plugin_item.tpl -->
<!-- END download -->
</ul>
</div>
<ul class="download">
<!-- BEGIN download -->
<!-- IMPORT admin/partials/download_plugin_item.tpl -->
<!-- END download -->
</ul>
</div>
<div class="tab-pane fade" id="upgrade">
<ul class="upgrade"></ul>
</div>
</div>
</div>

View File

@@ -0,0 +1,24 @@
<div class="social settings" class="row">
<form role="form">
<div class="row">
<div class="col-sm-2 col-xs-12 settings-header">Post Sharing</div>
<div class="col-sm-10 col-xs-12">
<div class="form-group" id="postSharingNetworks">
<!-- BEGIN posts -->
<div class="checkbox">
<label for="{posts.id}" class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input type="checkbox" class="mdl-switch__input" id="{posts.id}" data-field="{posts.id}" name="{posts.id}" <!-- IF posts.activated -->checked<!-- ENDIF posts.activated --> />
<span class="mdl-switch__label"><i class="fa {posts.class}"></i> {posts.name}</span>
</label>
</div>
<!-- END posts -->
<small>Plugins can add additional networks for sharing posts.</small>
</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>
</button>

View File

@@ -1,62 +1,82 @@
<div class="sounds settings" class="row">
<form role="form">
<div class="row">
<div class="col-sm-2 col-xs-12 settings-header">Notifications</div>
<div class="col-sm-10 col-xs-12">
<label for="notification">Notifications</label>
<div class="row">
<div class="form-group col-xs-9">
<select class="form-control" id="notification" name="notification">
<option value=""></option>
<!-- BEGIN sounds -->
<option value="{sounds.name}">{sounds.name}</option>
<!-- END sounds -->
</select>
</div>
<div class="btn-group col-xs-3">
<button type="button" class="form-control btn btn-sm btn-default" data-action="play">Play <i class="fa fa-play"></i></button>
<div class="col-xs-9">
<form role="form">
<div class="row">
<div class="col-sm-2 col-xs-12 settings-header">Notifications</div>
<div class="col-sm-10 col-xs-12">
<label for="notification">Notifications</label>
<div class="row">
<div class="form-group col-xs-9">
<select class="form-control" id="notification" name="notification">
<option value=""></option>
<!-- BEGIN sounds -->
<option value="{sounds.name}">{sounds.name}</option>
<!-- END sounds -->
</select>
</div>
<div class="btn-group col-xs-3">
<button type="button" class="form-control btn btn-sm btn-default" data-action="play">Play <i class="fa fa-play"></i></button>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-2 col-xs-12 settings-header">Chat Messages</div>
<div class="col-sm-10 col-xs-12">
<label for="chat-incoming">Incoming Message</label>
<div class="row">
<div class="form-group col-xs-9">
<select class="form-control" id="chat-incoming" name="chat-incoming">
<option value=""></option>
<!-- BEGIN sounds -->
<option value="{sounds.name}">{sounds.name}</option>
<!-- END sounds -->
</select>
<div class="row">
<div class="col-sm-2 col-xs-12 settings-header">Chat Messages</div>
<div class="col-sm-10 col-xs-12">
<label for="chat-incoming">Incoming Message</label>
<div class="row">
<div class="form-group col-xs-9">
<select class="form-control" id="chat-incoming" name="chat-incoming">
<option value=""></option>
<!-- BEGIN sounds -->
<option value="{sounds.name}">{sounds.name}</option>
<!-- END sounds -->
</select>
</div>
<div class="btn-group col-xs-3">
<button type="button" class="form-control btn btn-sm btn-default" data-action="play">Play <i class="fa fa-play"></i></button>
</div>
</div>
<div class="btn-group col-xs-3">
<button type="button" class="form-control btn btn-sm btn-default" data-action="play">Play <i class="fa fa-play"></i></button>
</div>
</div>
<label for="chat-outgoing">Outgoing Message</label>
<div class="row">
<div class="form-group col-xs-9">
<select class="form-control" id="chat-outgoing" name="chat-outgoing">
<option value=""></option>
<!-- BEGIN sounds -->
<option value="{sounds.name}">{sounds.name}</option>
<!-- END sounds -->
</select>
</div>
<div class="btn-group col-xs-3">
<button type="button" class="form-control btn btn-sm btn-default" data-action="play">Play <i class="fa fa-play"></i></button>
<label for="chat-outgoing">Outgoing Message</label>
<div class="row">
<div class="form-group col-xs-9">
<select class="form-control" id="chat-outgoing" name="chat-outgoing">
<option value=""></option>
<!-- BEGIN sounds -->
<option value="{sounds.name}">{sounds.name}</option>
<!-- END sounds -->
</select>
</div>
<div class="btn-group col-xs-3">
<button type="button" class="form-control btn btn-sm btn-default" data-action="play">Play <i class="fa fa-play"></i></button>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="col-xs-3">
<div class="panel">
<div class="panel-body">
<div class="input-group">
<span class="input-group-btn">
<input data-action="upload" data-target="logoUrl" data-route="{config.relative_path}/api/admin/upload/sound" type="button" class="btn btn-primary btn-block" value="Upload New Sound"></input>
</span>
</div>
</div>
</div>
</form>
</div>
</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>
</button>
</button>
<script>
require(['admin/settings'], function(Settings) {
Settings.init();
});
</script>

View File

@@ -3,6 +3,7 @@
<ul class="nav nav-pills">
<li class="active"><a href="#category-settings" data-toggle="tab">Category Settings</a></li>
<li><a href="#privileges" data-toggle="tab">Privileges</a></li>
<li><a href="#analytics" data-toggle="tab">Analytics</a></li>
</ul>
<br />
<div class="tab-content">
@@ -119,6 +120,37 @@
<!-- IMPORT admin/partials/categories/privileges.tpl -->
</div>
</div>
<div class="tab-pane fade col-xs-12" id="analytics">
<div class="row">
<div class="col-sm-6 text-center">
<div><canvas id="pageviews:hourly" height="250"></canvas></div>
<p>
<small><strong>Figure 1</strong> &ndash; Hourly page views for this category</small>
</p>
</div>
<div class="col-sm-6 text-center">
<div><canvas id="pageviews:daily" height="250"></canvas></div>
<p>
<small><strong>Figure 2</strong> &ndash; Daily page views for this category</small>
</p>
</div>
</div>
<div class="row">
<div class="col-sm-6 text-center">
<div><canvas id="topics:daily" height="250"></canvas></div>
<p>
<small><strong>Figure 3</strong> &ndash; Daily topics created in this category</small>
</p>
</div>
<div class="col-sm-6 text-center">
<div><canvas id="posts:daily" height="250"></canvas></div>
<p>
<small><strong>Figure 4</strong> &ndash; Daily posts made in this category</small>
</p>
</div>
</div>
</div>
</div>
</form>
</div>

View File

@@ -1,11 +1,7 @@
<div class="groups">
<div class="col-lg-9">
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-group"></i> Groups List</div>
<div class="panel-body">
<input id="group-search" type="text" class="form-control" placeholder="Search" /><br/>
<table class="table table-striped groups-list">
<tr>
<th>Group Name</th>
@@ -38,10 +34,9 @@
<div class="col-lg-3 acp-sidebar">
<div class="panel panel-default">
<div class="panel-heading">Groups Control Panel</div>
<div class="panel-body">
<div>
<button class="btn btn-primary" id="create">New Group</button>
<input id="group-search" type="text" class="form-control" placeholder="Search" />
</div>
</div>
</div>
@@ -78,4 +73,6 @@
</div>
<button id="create" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
<i class="material-icons">add</i>
</button>

View File

@@ -7,6 +7,7 @@
<li><a href="{relative_path}/admin/general/navigation">Navigation</a></li>
<li><a href="{relative_path}/admin/general/languages">Languages</a></li>
<li><a href="{relative_path}/admin/general/sounds">Sounds</a></li>
<li><a href="{relative_path}/admin/general/social">Social</a></li>
</ul>
</section>
@@ -160,6 +161,7 @@
<li><a href="{relative_path}/admin/general/navigation">Navigation</a></li>
<li><a href="{relative_path}/admin/general/languages">Languages</a></li>
<li><a href="{relative_path}/admin/general/sounds">Sounds</a></li>
<li><a href="{relative_path}/admin/general/social">Social</a></li>
</ul>
</li>
<li class="dropdown menu-item">

View File

@@ -85,7 +85,7 @@
</div>
<div class="form-group">
<label for="eventLoopLagThreshold">Event Loop Lag Threshold (in milliseconds)</label>
<input class="form-control" id="eventLoopLagThreshold" type="number" data-field="eventLoopLagThreshold" placeholder="Default: 70" step="10" value="70" />
<input class="form-control" id="eventLoopLagThreshold" type="number" data-field="eventLoopLagThreshold" placeholder="Default: 70" step="10" min="10" value="70" />
<p class="help-block">
Lowering this value decreases wait times for page loads, but will also show the
"excessive load" message to more users. (Reload required)
@@ -103,4 +103,4 @@
</div>
</div>
<!-- IMPORT admin/settings/footer.tpl -->
<!-- IMPORT admin/settings/footer.tpl -->

View File

@@ -174,6 +174,28 @@
<div class="col-sm-2 col-xs-12 settings-header">Default User Settings</div>
<div class="col-sm-10 col-xs-12">
<form>
<div class="checkbox">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input class="mdl-switch__input" type="checkbox" data-field="showemail">
<span class="mdl-switch__label"><strong>[[user:show_email]]</strong></span>
</label>
</div>
<div class="checkbox">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input class="mdl-switch__input" type="checkbox" data-field="showfullname">
<span class="mdl-switch__label"><strong>[[user:show_fullname]]</strong></span>
</label>
</div>
<div class="checkbox">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input class="mdl-switch__input" type="checkbox" data-field="restrictChat">
<span class="mdl-switch__label"><strong>[[user:restrict_chats]]</strong></span>
</label>
</div>
<div class="checkbox">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input class="mdl-switch__input" type="checkbox" data-field="openOutgoingLinksInNewTab">
@@ -226,6 +248,13 @@
</label>
</div>
<div class="checkbox">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input class="mdl-switch__input" type="checkbox" data-field="notificationSounds" />
<span class="mdl-switch__label">[[user:notification_sounds]]</span>
</label>
</div>
</form>
</div>
</div>

View File

@@ -42,7 +42,7 @@
<a href="{url}">[[email:digest.cta, {site_title}]]</a>
</p>
{footer}
<!-- IMPORT emails/partials/footer.tpl -->
<hr />
<p>

View File

@@ -8,4 +8,4 @@
<a href="{registerLink}">[[email:invitation.ctr]]</a>
</blockquote>
{footer}
<!-- IMPORT emails/partials/footer.tpl -->

View File

@@ -5,7 +5,7 @@
<a href="{url}/chats/{fromUserslug}">[[email:notif.chat.cta]]</a>
{footer}
<!-- IMPORT emails/partials/footer.tpl -->
<hr />
<p>

View File

@@ -5,7 +5,7 @@
<a href="{url}">[[email:notif.post.cta]]</a>
{footer}
<!-- IMPORT emails/partials/footer.tpl -->
<hr />
<p>

View File

@@ -1,2 +0,0 @@
[[email:closing]]
{site_title}

View File

@@ -6,4 +6,4 @@
<p>[[email:welcome.text3]]</p>
{footer}
<!-- IMPORT emails/partials/footer.tpl -->

View File

@@ -8,4 +8,4 @@
<a href="{reset_link}">[[email:reset.cta]]</a>
</blockquote>
{footer}
<!-- IMPORT emails/partials/footer.tpl -->

View File

@@ -4,4 +4,4 @@
<p>[[email:reset.notify.text2]]</p>
{footer}
<!-- IMPORT emails/partials/footer.tpl -->

View File

@@ -2,4 +2,4 @@
<p>[[email:test.text1]]</p>
{footer}
<!-- IMPORT emails/partials/footer.tpl -->

View File

@@ -10,4 +10,4 @@
<a href="{confirm_link}">[[email:welcome.cta]]</a>
</blockquote>
{footer}
<!-- IMPORT emails/partials/footer.tpl -->

View File

@@ -128,7 +128,9 @@ widgets.setArea = function(area, callback) {
widgets.reset = function(callback) {
var defaultAreas = [
{ name: 'Draft Zone', template: 'global', location: 'drafts' }
{ name: 'Draft Zone', template: 'global', location: 'header' },
{ name: 'Draft Zone', template: 'global', location: 'footer' },
{ name: 'Draft Zone', template: 'global', location: 'sidebar' }
];
plugins.fireHook('filter:widgets.getAreas', defaultAreas, function(err, areas) {