Merge branch 'develop' into threads-enhancement

This commit is contained in:
psychobunny
2017-02-08 16:41:16 -05:00
458 changed files with 5845 additions and 3877 deletions

View File

@@ -4,6 +4,7 @@ var fs = require('fs');
var path = require('path');
var async = require('async');
var sanitizeHTML = require('sanitize-html');
var nconf = require('nconf');
var utils = require('../../public/src/utils');
var Translator = require('../../public/src/modules/translator').Translator;
@@ -23,7 +24,7 @@ function filterDirectories(directories) {
}
function getAdminNamespaces(callback) {
utils.walk(path.resolve(__dirname, '../../public/templates/admin'), function (err, directories) {
utils.walk(path.resolve(nconf.get('views_dir'), 'admin'), function (err, directories) {
if (err) {
return callback(err);
}
@@ -60,7 +61,7 @@ var fallbackCacheInProgress = {};
var fallbackCache = {};
function initFallback(namespace, callback) {
fs.readFile(path.resolve(__dirname, '../../public/templates/', namespace + '.tpl'), function (err, file) {
fs.readFile(path.resolve(nconf.get('views_dir'), namespace + '.tpl'), function (err, file) {
if (err) {
return callback(err);
}

View File

@@ -16,17 +16,21 @@ var uniquevisitors = 0;
var isCategory = /^(?:\/api)?\/category\/(\d+)/;
new cronJob('*/10 * * * *', function () {
new cronJob('*/10 * * * * *', function () {
Analytics.writeData();
}, null, true);
Analytics.increment = function (keys) {
Analytics.increment = function (keys, callback) {
keys = Array.isArray(keys) ? keys : [keys];
keys.forEach(function (key) {
counters[key] = counters[key] || 0;
++counters[key];
});
if (typeof callback === 'function') {
callback();
}
};
Analytics.pageView = function (payload) {

View File

@@ -73,7 +73,7 @@ module.exports = function (Categories) {
next(null, category);
},
function (category, next) {
plugins.fireHook('action:category.create', category);
plugins.fireHook('action:category.create', {category: category});
next(null, category);
}
], callback);
@@ -139,9 +139,20 @@ module.exports = function (Categories) {
};
Categories.copyPrivilegesFrom = function (fromCid, toCid, callback) {
async.each(privileges.privilegeList, function (privilege, next) {
copyPrivilege(privilege, fromCid, toCid, next);
}, callback);
async.waterfall([
function (next) {
plugins.fireHook('filter:categories.copyPrivilegesFrom', {
privileges: privileges.privilegeList,
fromCid: fromCid,
toCid: toCid
}, next);
},
function (data, next) {
async.each(data.privileges, function (privilege, next) {
copyPrivilege(privilege, data.fromCid, data.toCid, next);
}, next);
}
], callback);
};
function copyPrivilege(privilege, fromCid, toCid, callback) {

View File

@@ -31,7 +31,7 @@ module.exports = function (Categories) {
purgeCategory(cid, next);
},
function (next) {
plugins.fireHook('action:category.delete', cid);
plugins.fireHook('action:category.delete', {cid: cid, uid: uid});
next();
}
], callback);

View File

@@ -119,7 +119,13 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
userData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1;
userData['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1;
userData['email:confirmed'] = !!parseInt(userData['email:confirmed'], 10);
userData.profile_links = filterLinks(results.profile_links.concat(results.profile_menu.links), isSelf);
userData.profile_links = filterLinks(results.profile_links.concat(results.profile_menu.links), {
self: isSelf,
other: !isSelf,
moderator: isModerator,
globalMod: isGlobalModerator,
admin: isAdmin
});
userData.sso = results.sso.associations;
userData.status = user.getStatus(userData);
@@ -154,9 +160,30 @@ helpers.getBaseUser = function (userslug, callerUID, callback) {
helpers.getUserDataByUserSlug(userslug, callerUID, callback);
};
function filterLinks(links, self) {
return links.filter(function (link) {
return link && (link.public || self);
function filterLinks(links, states) {
return links.filter(function (link, index) {
// "public" is the old property, if visibility is defined, discard `public`
if (link.hasOwnProperty('public') && !link.hasOwnProperty('visibility')) {
winston.warn('[account/profileMenu (' + link.id + ')] Use of the `.public` property is deprecated, use `visibility` now');
return link && (link.public || states.self);
}
// Default visibility
link.visibility = Object.assign({
self: true,
other: true,
moderator: true,
globalMod: true,
admin: true
}, link.visibility);
// Iterate through states and permit if every test passes (or is not defined)
var permit = Object.keys(states).some(function (state) {
return states[state] === link.visibility[state];
});
links[index].public = permit;
return permit;
});
}

View File

@@ -4,7 +4,6 @@ var adminController = {
dashboard: require('./admin/dashboard'),
categories: require('./admin/categories'),
tags: require('./admin/tags'),
flags: require('./admin/flags'),
blacklist: require('./admin/blacklist'),
groups: require('./admin/groups'),
appearance: require('./admin/appearance'),

View File

@@ -1,102 +0,0 @@
"use strict";
var async = require('async');
var validator = require('validator');
var posts = require('../../posts');
var user = require('../../user');
var categories = require('../../categories');
var analytics = require('../../analytics');
var pagination = require('../../pagination');
var flagsController = {};
var itemsPerPage = 20;
flagsController.get = function (req, res, next) {
var byUsername = req.query.byUsername || '';
var cid = req.query.cid || 0;
var sortBy = req.query.sortBy || 'count';
var page = parseInt(req.query.page, 10) || 1;
async.parallel({
categories: function (next) {
categories.buildForSelect(req.uid, next);
},
flagData: function (next) {
getFlagData(req, res, next);
},
analytics: function (next) {
analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next);
},
assignees: async.apply(user.getAdminsandGlobalModsandModerators)
}, function (err, results) {
if (err) {
return next(err);
}
// Minimise data set for assignees so tjs does less work
results.assignees = results.assignees.map(function (userObj) {
return {
uid: userObj.uid,
username: userObj.username
};
});
// If res.locals.cids is populated, then slim down the categories list
if (res.locals.cids) {
results.categories = results.categories.filter(function (category) {
return res.locals.cids.indexOf(String(category.cid)) !== -1;
});
}
var pageCount = Math.max(1, Math.ceil(results.flagData.count / itemsPerPage));
results.categories.forEach(function (category) {
category.selected = parseInt(category.cid, 10) === parseInt(cid, 10);
});
var data = {
posts: results.flagData.posts,
assignees: results.assignees,
analytics: results.analytics,
categories: results.categories,
byUsername: validator.escape(String(byUsername)),
sortByCount: sortBy === 'count',
sortByTime: sortBy === 'time',
pagination: pagination.create(page, pageCount, req.query),
};
res.render('admin/manage/flags', data);
});
};
function getFlagData(req, res, callback) {
var sortBy = req.query.sortBy || 'count';
var byUsername = req.query.byUsername || '';
var cid = req.query.cid || res.locals.cids || 0;
var page = parseInt(req.query.page, 10) || 1;
var start = (page - 1) * itemsPerPage;
var stop = start + itemsPerPage - 1;
var sets = [sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged'];
async.waterfall([
function (next) {
if (byUsername) {
user.getUidByUsername(byUsername, next);
} else {
process.nextTick(next, null, 0);
}
},
function (uid, next) {
if (uid) {
sets.push('uid:' + uid + ':flag:pids');
}
posts.getFlags(sets, cid, req.uid, start, stop, next);
}
], callback);
}
module.exports = flagsController;

View File

@@ -2,6 +2,7 @@
var async = require('async');
var nconf = require('nconf');
var meta = require('../../meta');
var settingsController = module.exports;
@@ -25,7 +26,7 @@ function renderEmail(req, res, next) {
var path = require('path');
var utils = require('../../../public/src/utils');
var emailsPath = path.join(__dirname, '../../../public/templates/emails');
var emailsPath = path.join(nconf.get('views_dir'), 'emails');
async.waterfall([
function (next) {

View File

@@ -7,9 +7,9 @@ var themesController = {};
themesController.get = function (req, res, next) {
var themeDir = path.join(__dirname, '../../../node_modules/' + req.params.theme);
file.exists(themeDir, function (exists) {
if (!exists) {
return next();
file.exists(themeDir, function (err, exists) {
if (err || !exists) {
return next(err);
}
var themeConfig = require(path.join(themeDir, 'theme.json')),

View File

@@ -105,14 +105,10 @@ uploadsController.uploadSound = function (req, res, next) {
return next(err);
}
var soundsPath = path.join(__dirname, '../../../public/sounds'),
var soundsPath = path.join(__dirname, '../../../build/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');
}
file.link(filePath, path.join(soundsPath, path.basename(filePath)));
fs.unlink(uploadedFile.path, function (err) {
if (err) {

View File

@@ -128,7 +128,11 @@ function getUsers(set, section, min, max, req, res, next) {
if (byScore) {
db.sortedSetCount(set, min, max, next);
} else {
db.sortedSetCard(set, next);
if (set === 'users:banned' || set === 'users:notvalidated') {
db.sortedSetCard(set, next);
} else {
db.getObjectField('global', 'userCount', next);
}
}
},
users: function (next) {

View File

@@ -332,7 +332,7 @@ authenticationController.onSuccessfulLogin = function (req, uid, callback) {
// Force session check for all connected socket.io clients with the same session id
sockets.in('sess_' + req.sessionID).emit('checkSession', uid);
plugins.fireHook('action:user.loggedIn', uid);
plugins.fireHook('action:user.loggedIn', {uid: uid, req: req});
callback();
});
};

View File

@@ -3,24 +3,127 @@
var async = require('async');
var user = require('../user');
var adminFlagsController = require('./admin/flags');
var categories = require('../categories');
var flags = require('../flags');
var analytics = require('../analytics');
var modsController = {};
var modsController = {
flags: {}
};
modsController.flagged = function (req, res, next) {
modsController.flags.list = function (req, res, next) {
async.parallel({
isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid),
moderatedCids: async.apply(user.getModeratedCids, req.uid)
}, function (err, results) {
if (err || !(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
if (err) {
return next(err);
} else if (!(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
return next(new Error('[[error:no-privileges]]'));
}
if (!results.isAdminOrGlobalMod && results.moderatedCids.length) {
res.locals.cids = results.moderatedCids;
}
adminFlagsController.get(req, res, next);
// Parse query string params for filters
var hasFilter = false;
var valid = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick'];
var filters = valid.reduce(function (memo, cur) {
if (req.query.hasOwnProperty(cur)) {
memo[cur] = req.query[cur];
}
return memo;
}, {});
hasFilter = !!Object.keys(filters).length;
if (res.locals.cids) {
if (!filters.cid) {
// If mod and no cid filter, add filter for their modded categories
filters.cid = res.locals.cids;
} else {
// Remove cids they do not moderate
if (Array.isArray(filters.cid)) {
filters.cid = filters.cid.filter(function (cid) {
return res.locals.cids.indexOf(String(cid)) !== -1;
});
} else if (res.locals.cids.indexOf(String(filters.cid)) === -1) {
filters.cid = res.locals.cids;
hasFilter = false;
}
}
}
async.parallel({
flags: async.apply(flags.list, filters, req.uid),
analytics: async.apply(analytics.getDailyStatsForSet, 'analytics:flags', Date.now(), 30),
categories: async.apply(categories.buildForSelect, req.uid)
}, function (err, data) {
if (err) {
return next(err);
}
// If res.locals.cids is populated, then slim down the categories list
if (res.locals.cids) {
data.categories = data.categories.filter(function (category) {
return res.locals.cids.indexOf(String(category.cid)) !== -1;
});
}
// Minimal returned set for templates.js
data.categories = data.categories.reduce(function (memo, cur) {
if (!res.locals.cids) {
memo[cur.cid] = cur.name;
return memo;
}
// If mod, remove categories they can't moderate
if (res.locals.cids.indexOf(String(cur.cid)) !== -1) {
memo[cur.cid] = cur.name;
}
return memo;
}, {});
res.render('flags/list', {
flags: data.flags,
analytics: data.analytics,
categories: data.categories,
hasFilter: hasFilter,
filters: filters,
title: '[[pages:flags]]'
});
});
});
};
modsController.flags.detail = function (req, res, next) {
async.parallel({
isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid),
moderatedCids: async.apply(user.getModeratedCids, req.uid),
flagData: async.apply(flags.get, req.params.flagId),
assignees: async.apply(user.getAdminsandGlobalModsandModerators)
}, function (err, results) {
if (err || !results.flagData) {
return next(err || new Error('[[error:invalid-data]]'));
} else if (!(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
return next(new Error('[[error:no-privileges]]'));
}
res.render('flags/detail', Object.assign(results.flagData, {
assignees: results.assignees,
type_bool: ['post', 'user', 'empty'].reduce(function (memo, cur) {
if (cur !== 'empty') {
memo[cur] = results.flagData.type === cur && !!Object.keys(results.flagData.target).length;
} else {
memo[cur] = !Object.keys(results.flagData.target).length;
}
return memo;
}, {}),
title: '[[pages:flag-details, ' + req.params.flagId + ']]'
}));
});
};

View File

@@ -38,7 +38,7 @@ searchController.search = function (req, res, next) {
repliesFilter: req.query.repliesFilter,
timeRange: req.query.timeRange,
timeFilter: req.query.timeFilter,
sortBy: req.query.sortBy,
sortBy: req.query.sortBy || meta.config.searchDefaultSortBy || '',
sortDirection: req.query.sortDirection,
page: page,
uid: req.uid,
@@ -67,6 +67,7 @@ searchController.search = function (req, res, next) {
searchData.title = '[[global:header.search]]';
searchData.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:search]]'}]);
searchData.expandSearch = !req.query.term;
searchData.searchDefaultSortBy = meta.config.searchDefaultSortBy || '';
res.render('search', searchData);
});

View File

@@ -40,7 +40,15 @@ unreadController.get = function (req, res, next) {
settings = results.settings;
var start = Math.max(0, (page - 1) * settings.topicsPerPage);
var stop = start + settings.topicsPerPage - 1;
topics.getUnreadTopics(cid, req.uid, start, stop, filter, next);
var cutoff = req.session.unreadCutoff ? req.session.unreadCutoff : topics.unreadCutoff();
topics.getUnreadTopics({
cid: cid,
uid: req.uid,
start: start,
stop: stop,
filter: filter,
cutoff: cutoff
}, next);
}
], function (err, data) {
if (err) {

View File

@@ -26,7 +26,7 @@ function getCover(type, id) {
return covers[id];
}
return nconf.get('relative_path') + '/images/cover-default.png';
return nconf.get('relative_path') + '/assets/images/cover-default.png';
}
module.exports = coverPhoto;

View File

@@ -87,19 +87,40 @@ file.allowedExtensions = function () {
file.exists = function (path, callback) {
fs.stat(path, function (err, stat) {
callback(!err && stat);
if (err) {
if (err.code === 'ENOENT') {
return callback(null, false);
}
return callback(err);
}
return callback(null, true);
});
};
file.existsSync = function (path) {
var exists = false;
try {
exists = fs.statSync(path);
} catch(err) {
exists = false;
fs.statSync(path);
} catch (err) {
if (err.code === 'ENOENT') {
return false;
}
throw err;
}
return !!exists;
return true;
};
file.link = function link(filePath, destPath, cb) {
if (process.platform === 'win32') {
fs.link(filePath, destPath, cb);
} else {
fs.symlink(filePath, destPath, 'file', cb);
}
};
file.linkDirs = function linkDirs(sourceDir, destDir, callback) {
var type = (process.platform === 'win32') ? 'junction' : 'dir';
fs.symlink(sourceDir, destDir, type, callback);
};
module.exports = file;

663
src/flags.js Normal file
View File

@@ -0,0 +1,663 @@
'use strict';
var async = require('async');
var winston = require('winston');
var db = require('./database');
var user = require('./user');
var groups = require('./groups');
var meta = require('./meta');
var notifications = require('./notifications');
var analytics = require('./analytics');
var topics = require('./topics');
var posts = require('./posts');
var privileges = require('./privileges');
var plugins = require('./plugins');
var utils = require('../public/src/utils');
var _ = require('underscore');
var S = require('string');
var Flags = {};
Flags.get = function (flagId, callback) {
async.waterfall([
// First stage
async.apply(async.parallel, {
base: async.apply(db.getObject.bind(db), 'flag:' + flagId),
history: async.apply(Flags.getHistory, flagId),
notes: async.apply(Flags.getNotes, flagId)
}),
function (data, next) {
// Second stage
async.parallel({
userObj: async.apply(user.getUserFields, data.base.uid, ['username', 'userslug', 'picture']),
targetObj: async.apply(Flags.getTarget, data.base.type, data.base.targetId, data.base.uid)
}, function (err, payload) {
// Final object return construction
next(err, Object.assign(data.base, {
datetimeISO: new Date(parseInt(data.base.datetime, 10)).toISOString(),
target_readable: data.base.type.charAt(0).toUpperCase() + data.base.type.slice(1) + ' ' + data.base.targetId,
target: payload.targetObj,
history: data.history,
notes: data.notes,
reporter: payload.userObj
}));
});
}
], callback);
};
Flags.list = function (filters, uid, callback) {
if (typeof filters === 'function' && !uid && !callback) {
callback = filters;
filters = {};
}
var sets = [];
var orSets = [];
var prepareSets = function (setPrefix, value) {
if (!Array.isArray(value)) {
sets.push(setPrefix + value);
} else if (value.length) {
value.forEach(function (x) {
orSets.push(setPrefix + x);
});
} else {
// Empty array, do nothing
return;
}
};
if (Object.keys(filters).length > 0) {
for (var type in filters) {
switch (type) {
case 'type':
prepareSets('flags:byType:', filters[type]);
break;
case 'state':
prepareSets('flags:byState:', filters[type]);
break;
case 'reporterId':
prepareSets('flags:byReporter:', filters[type]);
break;
case 'assignee':
prepareSets('flags:byAssignee:', filters[type]);
break;
case 'targetUid':
prepareSets('flags:byTargetUid:', filters[type]);
break;
case 'cid':
prepareSets('flags:byCid:', filters[type]);
break;
case 'quick':
switch (filters.quick) {
case 'mine':
sets.push('flags:byAssignee:' + uid);
break;
}
break;
}
}
}
sets = (sets.length || orSets.length) ? sets : ['flags:datetime']; // No filter default
async.waterfall([
function (next) {
if (sets.length === 1) {
db.getSortedSetRevRange(sets[0], 0, -1, next);
} else if (sets.length > 1) {
db.getSortedSetRevIntersect({ sets: sets, start: 0, stop: -1, aggregate: 'MAX' }, next);
} else {
next(null, []);
}
},
function (flagIds, next) {
// Find flags according to "or" rules, if any
if (orSets.length) {
db.getSortedSetRevUnion({ sets: orSets, start: 0, stop: -1, aggregate: 'MAX' }, function (err, _flagIds) {
if (err) {
return next(err);
}
if (sets.length) {
// If flag ids are already present, return a subset of flags that are in both sets
next(null, _.intersection(flagIds, _flagIds));
} else {
// Otherwise, return all flags returned via orSets
next(null, _.union(flagIds, _flagIds));
}
});
} else {
setImmediate(next, null, flagIds);
}
},
function (flagIds, next) {
async.map(flagIds, function (flagId, next) {
async.waterfall([
async.apply(db.getObject, 'flag:' + flagId),
function (flagObj, next) {
user.getUserFields(flagObj.uid, ['username', 'picture'], function (err, userObj) {
next(err, Object.assign(flagObj, {
reporter: {
username: userObj.username,
picture: userObj.picture,
'icon:bgColor': userObj['icon:bgColor'],
'icon:text': userObj['icon:text']
}
}));
});
}
], function (err, flagObj) {
if (err) {
return next(err);
}
switch(flagObj.state) {
case 'open':
flagObj.labelClass = 'info';
break;
case 'wip':
flagObj.labelClass = 'warning';
break;
case 'resolved':
flagObj.labelClass = 'success';
break;
case 'rejected':
flagObj.labelClass = 'danger';
break;
}
next(null, Object.assign(flagObj, {
target_readable: flagObj.type.charAt(0).toUpperCase() + flagObj.type.slice(1) + ' ' + flagObj.targetId,
datetimeISO: new Date(parseInt(flagObj.datetime, 10)).toISOString()
}));
});
}, next);
}
], callback);
};
Flags.validate = function (payload, callback) {
async.parallel({
targetExists: async.apply(Flags.targetExists, payload.type, payload.id),
target: async.apply(Flags.getTarget, payload.type, payload.id, payload.uid),
reporter: async.apply(user.getUserData, payload.uid)
}, function (err, data) {
if (err) {
return callback(err);
}
if (data.target.deleted) {
return callback(new Error('[[error:post-deleted]]'));
} else if (parseInt(data.reporter.banned, 10)) {
return callback(new Error('[[error:user-banned]]'));
}
switch (payload.type) {
case 'post':
privileges.posts.canEdit(payload.id, payload.uid, function (err, editable) {
if (err) {
return callback(err);
}
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
// Check if reporter meets rep threshold (or can edit the target post, in which case threshold does not apply)
if (!editable.flag && parseInt(data.reporter.reputation, 10) < minimumReputation) {
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
}
callback();
});
break;
case 'user':
privileges.users.canEdit(payload.uid, payload.id, function (err, editable) {
if (err) {
return callback(err);
}
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
// Check if reporter meets rep threshold (or can edit the target user, in which case threshold does not apply)
if (!editable && parseInt(data.reporter.reputation, 10) < minimumReputation) {
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
}
callback();
});
break;
default:
callback(new Error('[[error:invalid-data]]'));
break;
}
});
};
Flags.getNotes = function (flagId, callback) {
async.waterfall([
async.apply(db.getSortedSetRevRangeWithScores.bind(db), 'flag:' + flagId + ':notes', 0, -1),
function (notes, next) {
var uids = [];
var noteObj;
notes = notes.map(function (note) {
try {
noteObj = JSON.parse(note.value);
uids.push(noteObj[0]);
return {
uid: noteObj[0],
content: noteObj[1],
datetime: note.score,
datetimeISO: new Date(parseInt(note.score, 10)).toISOString()
};
} catch (e) {
return next(e);
}
});
next(null, notes, uids);
},
function (notes, uids, next) {
user.getUsersFields(uids, ['username', 'userslug', 'picture'], function (err, users) {
if (err) {
return next(err);
}
next(null, notes.map(function (note, idx) {
note.user = users[idx];
return note;
}));
});
}
], callback);
};
Flags.create = function (type, id, uid, reason, timestamp, callback) {
var targetUid;
var targetCid;
var doHistoryAppend = false;
// timestamp is optional
if (typeof timestamp === 'function' && !callback) {
callback = timestamp;
timestamp = Date.now();
doHistoryAppend = true;
}
async.waterfall([
function (next) {
async.parallel([
// Sanity checks
async.apply(Flags.exists, type, id, uid),
async.apply(Flags.targetExists, type, id),
// Extra data for zset insertion
async.apply(Flags.getTargetUid, type, id),
async.apply(Flags.getTargetCid, type, id)
], function (err, checks) {
if (err) {
return next(err);
}
targetUid = checks[2] || null;
targetCid = checks[3] || null;
if (checks[0]) {
return next(new Error('[[error:already-flagged]]'));
} else if (!checks[1]) {
return next(new Error('[[error:invalid-data]]'));
} else {
next();
}
});
},
async.apply(db.incrObjectField, 'global', 'nextFlagId'),
function (flagId, next) {
var tasks = [
async.apply(db.setObject.bind(db), 'flag:' + flagId, {
flagId: flagId,
type: type,
targetId: id,
description: reason,
uid: uid,
datetime: timestamp
}),
async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', timestamp, flagId), // by time, the default
async.apply(db.sortedSetAdd.bind(db), 'flags:byReporter:' + uid, timestamp, flagId), // by reporter
async.apply(db.sortedSetAdd.bind(db), 'flags:byType:' + type, timestamp, flagId), // by flag type
async.apply(db.sortedSetAdd.bind(db), 'flags:hash', flagId, [type, id, uid].join(':')), // save zset for duplicate checking
async.apply(analytics.increment, 'flags') // some fancy analytics
];
if (targetUid) {
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byTargetUid:' + targetUid, timestamp, flagId)); // by target uid
}
if (targetCid) {
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byCid:' + targetCid, timestamp, flagId)); // by target cid
}
if (type === 'post') {
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byPid:' + id, timestamp, flagId)); // by target pid
}
async.parallel(tasks, function (err, data) {
if (err) {
return next(err);
}
if (doHistoryAppend) {
Flags.update(flagId, uid, { "state": "open" });
}
next(null, flagId);
});
},
async.apply(Flags.get)
], callback);
};
Flags.exists = function (type, id, uid, callback) {
db.isSortedSetMember('flags:hash', [type, id, uid].join(':'), callback);
};
Flags.getTarget = function (type, id, uid, callback) {
async.waterfall([
async.apply(Flags.targetExists, type, id),
function (exists, next) {
if (exists) {
switch (type) {
case 'post':
async.waterfall([
async.apply(posts.getPostsByPids, [id], uid),
function (posts, next) {
topics.addPostData(posts, uid, next);
}
], function (err, posts) {
next(err, posts[0]);
});
break;
case 'user':
user.getUsersData([id], function (err, users) {
next(err, users ? users[0] : undefined);
});
break;
default:
next(new Error('[[error:invalid-data]]'));
break;
}
} else {
// Target used to exist (otherwise flag creation'd fail), but no longer
next(null, {});
}
}
], callback);
};
Flags.targetExists = function (type, id, callback) {
switch (type) {
case 'post':
posts.exists(id, callback);
break;
case 'user':
user.exists(id, callback);
break;
default:
callback(new Error('[[error:invalid-data]]'));
break;
}
};
Flags.getTargetUid = function (type, id, callback) {
switch (type) {
case 'post':
posts.getPostField(id, 'uid', callback);
break;
default:
setImmediate(callback, null, id);
break;
}
};
Flags.getTargetCid = function (type, id, callback) {
switch (type) {
case 'post':
posts.getCidByPid(id, callback);
break;
default:
setImmediate(callback, null, id);
break;
}
};
Flags.update = function (flagId, uid, changeset, callback) {
// Retrieve existing flag data to compare for history-saving purposes
var fields = ['state', 'assignee'];
var tasks = [];
var now = changeset.datetime || Date.now();
async.waterfall([
async.apply(db.getObjectFields.bind(db), 'flag:' + flagId, fields),
function (current, next) {
for (var prop in changeset) {
if (changeset.hasOwnProperty(prop)) {
if (current[prop] === changeset[prop]) {
delete changeset[prop];
} else {
// Add tasks as necessary
switch (prop) {
case 'state':
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byState:' + changeset[prop], now, flagId));
tasks.push(async.apply(db.sortedSetRemove.bind(db), 'flags:byState:' + current[prop], flagId));
break;
case 'assignee':
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byAssignee:' + changeset[prop], now, flagId));
break;
}
}
}
}
if (!Object.keys(changeset).length) {
// No changes
return next();
}
// Save new object to db (upsert)
tasks.push(async.apply(db.setObject, 'flag:' + flagId, changeset));
// Append history
tasks.push(async.apply(Flags.appendHistory, flagId, uid, changeset));
// Fire plugin hook
tasks.push(async.apply(plugins.fireHook, 'action:flag.update', { flagId: flagId, changeset: changeset, uid: uid }));
async.parallel(tasks, function (err, data) {
return next(err);
});
}
], callback);
};
Flags.getHistory = function (flagId, callback) {
var history;
var uids = [];
async.waterfall([
async.apply(db.getSortedSetRevRangeWithScores.bind(db), 'flag:' + flagId + ':history', 0, -1),
function (_history, next) {
history = _history.map(function (entry) {
try {
entry.value = JSON.parse(entry.value);
} catch (e) {
return callback(e);
}
uids.push(entry.value[0]);
// Deserialise changeset
var changeset = entry.value[1];
if (changeset.hasOwnProperty('state')) {
changeset.state = changeset.state === undefined ? '' : '[[flags:state-' + changeset.state + ']]';
}
return {
uid: entry.value[0],
fields: changeset,
datetime: entry.score,
datetimeISO: new Date(parseInt(entry.score, 10)).toISOString()
};
});
user.getUsersFields(uids, ['username', 'userslug', 'picture'], next);
}
], function (err, users) {
if (err) {
return callback(err);
}
// Append user data to each history event
history = history.map(function (event, idx) {
event.user = users[idx];
return event;
});
callback(null, history);
});
};
Flags.appendHistory = function (flagId, uid, changeset, callback) {
var payload;
var datetime = changeset.datetime || Date.now();
delete changeset.datetime;
try {
payload = JSON.stringify([uid, changeset, datetime]);
} catch (e) {
return callback(e);
}
db.sortedSetAdd('flag:' + flagId + ':history', datetime, payload, callback);
};
Flags.appendNote = function (flagId, uid, note, datetime, callback) {
if (typeof datetime === 'function' && !callback) {
callback = datetime;
datetime = Date.now();
}
var payload;
try {
payload = JSON.stringify([uid, note]);
} catch (e) {
return callback(e);
}
async.waterfall([
async.apply(db.sortedSetAdd, 'flag:' + flagId + ':notes', datetime, payload),
async.apply(Flags.appendHistory, flagId, uid, {
notes: null,
datetime: datetime
})
], callback);
};
Flags.notify = function (flagObj, uid, callback) {
// Notify administrators, mods, and other associated people
if (!callback) {
callback = function () {};
}
switch (flagObj.type) {
case 'post':
async.parallel({
post: function (next) {
async.waterfall([
async.apply(posts.getPostData, flagObj.targetId),
async.apply(posts.parsePost)
], next);
},
title: async.apply(topics.getTitleByPid, flagObj.targetId),
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
globalMods: async.apply(groups.getMembers, 'Global Moderators', 0, -1),
moderators: function (next) {
async.waterfall([
async.apply(posts.getCidByPid, flagObj.targetId),
function (cid, next) {
groups.getMembers('cid:' + cid + ':privileges:mods', 0, -1, next);
}
], next);
}
}, function (err, results) {
if (err) {
return callback(err);
}
var title = S(results.title).decodeHTMLEntities().s;
var titleEscaped = title.replace(/%/g, '&#37;').replace(/,/g, '&#44;');
notifications.create({
bodyShort: '[[notifications:user_flagged_post_in, ' + flagObj.reporter.username + ', ' + titleEscaped + ']]',
bodyLong: flagObj.description,
pid: flagObj.targetId,
path: '/post/' + flagObj.targetId,
nid: 'flag:post:' + flagObj.targetId + ':uid:' + uid,
from: uid,
mergeId: 'notifications:user_flagged_post_in|' + flagObj.targetId,
topicTitle: results.title
}, function (err, notification) {
if (err || !notification) {
return callback(err);
}
plugins.fireHook('action:flag.create', {
flag: flagObj
});
notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), callback);
});
});
break;
case 'user':
async.parallel({
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
globalMods: async.apply(groups.getMembers, 'Global Moderators', 0, -1),
}, function (err, results) {
if (err) {
return callback(err);
}
notifications.create({
bodyShort: '[[notifications:user_flagged_user, ' + flagObj.reporter.username + ', ' + flagObj.target.username + ']]',
bodyLong: flagObj.description,
path: '/uid/' + flagObj.targetId,
nid: 'flag:user:' + flagObj.targetId + ':uid:' + uid,
from: uid,
mergeId: 'notifications:user_flagged_user|' + flagObj.targetId
}, function (err, notification) {
if (err || !notification) {
return callback(err);
}
plugins.fireHook('action:flag.create', {
flag: flagObj
});
notifications.push(notification, results.admins.concat(results.globalMods), callback);
});
});
break;
default:
callback(new Error('[[error:invalid-data]]'));
break;
}
};
module.exports = Flags;

View File

@@ -432,12 +432,8 @@ var utils = require('../public/src/utils');
db.getSortedSetRevRange(set, 0, -1, next);
},
function (groupNames, next) {
var groupSets = groupNames.map(function (name) {
return 'group:' + name + ':members';
});
async.map(uids, function (uid, next) {
db.isMemberOfSortedSets(groupSets, uid, function (err, isMembers) {
Groups.isMemberOfGroups(uid, groupNames, function (err, isMembers) {
if (err) {
return next(err);
}

View File

@@ -70,7 +70,7 @@ module.exports = function (Groups) {
async.series(tasks, next);
},
function (results, next) {
plugins.fireHook('action:group.create', groupData);
plugins.fireHook('action:group.create', {group: groupData});
next(null, groupData);
}
], callback);

View File

@@ -17,8 +17,6 @@ module.exports = function (Groups) {
}
var groupObj = groupsData[0];
plugins.fireHook('action:group.destroy', groupObj);
async.parallel([
async.apply(db.delete, 'group:' + groupName),
async.apply(db.sortedSetRemove, 'groups:createtime', groupName),
@@ -45,6 +43,7 @@ module.exports = function (Groups) {
return callback(err);
}
Groups.resetCache();
plugins.fireHook('action:group.destroy', {group: groupObj});
callback();
});
});

View File

@@ -65,6 +65,9 @@ image.resizeImage = function (data, callback) {
}
},
function (image, next) {
if (data.write === false) {
return next();
}
image.write(data.target || data.path, next);
}
], function (err) {

View File

@@ -15,7 +15,14 @@ module.exports = function (Messaging) {
Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser
Messaging.notifyUsersInRoom = function (fromUid, roomId, messageObj) {
Messaging.getUidsInRoom(roomId, 0, -1, function (err, uids) {
async.parallel({
uids: function (next) {
Messaging.getUidsInRoom(roomId, 0, -1, next);
},
roomData: function (next) {
Messaging.getRoomData(roomId, next);
}
}, function (err, results) {
if (err) {
return;
}
@@ -23,9 +30,10 @@ module.exports = function (Messaging) {
var data = {
roomId: roomId,
fromUid: fromUid,
message: messageObj
message: messageObj,
roomName: results.roomData.roomName
};
uids.forEach(function (uid) {
results.uids.forEach(function (uid) {
data.self = parseInt(uid, 10) === parseInt(fromUid) ? 1 : 0;
Messaging.pushUnreadCount(uid);
sockets.in('uid_' + uid).emit('event:chats.receive', data);
@@ -43,7 +51,7 @@ module.exports = function (Messaging) {
}
queueObj.timeout = setTimeout(function () {
sendNotifications(fromUid, uids, roomId, queueObj.message, function (err) {
sendNotifications(fromUid, results.uids, roomId, queueObj.message, function (err) {
if (!err) {
delete Messaging.notifyQueue[fromUid + ':' + roomId];
}

View File

@@ -43,20 +43,36 @@ exports.build = function build(targets, callback) {
};
exports.buildTargets = function (targets, callback) {
var cacheBuster = require('./cacheBuster');
var meta = require('../meta');
var numCpus = require('os').cpus().length;
var strategy = (targets.length > 1 && numCpus > 1);
buildStart = buildStart || Date.now();
var step = function (startTime, target, next) {
var step = function (startTime, target, next, err) {
if (err) {
winston.error('Build failed: ' + err.message);
process.exit(1);
}
winston.info('[build] ' + target + ' => Completed in ' + ((Date.now() - startTime) / 1000) + 's');
next();
};
async.parallel([
if (strategy) {
winston.verbose('[build] Utilising multiple cores/processes');
} else {
winston.verbose('[build] Utilising single-core');
}
async[strategy ? 'parallel' : 'series']([
function (next) {
if (targets.indexOf('js') !== -1) {
winston.info('[build] Building javascript');
var startTime = Date.now();
async.series([
meta.js.linkModules,
meta.js.linkStatics,
async.apply(meta.js.minify, 'nodebb.min.js'),
async.apply(meta.js.minify, 'acp.min.js')
], step.bind(this, startTime, 'js', next));
@@ -74,13 +90,13 @@ exports.buildTargets = function (targets, callback) {
case 'clientCSS':
winston.info('[build] Building client-side CSS');
startTime = Date.now();
meta.css.minify('stylesheet.css', step.bind(this, startTime, target, next));
meta.css.minify('client', step.bind(this, startTime, target, next));
break;
case 'acpCSS':
winston.info('[build] Building admin control panel CSS');
startTime = Date.now();
meta.css.minify('admin.css', step.bind(this, startTime, target, next));
meta.css.minify('admin', step.bind(this, startTime, target, next));
break;
case 'tpl':
@@ -108,14 +124,21 @@ exports.buildTargets = function (targets, callback) {
return process.exit(1);
}
var time = (Date.now() - buildStart) / 1000;
cacheBuster.write(function (err) {
if (err) {
winston.error('[build] Failed to write `cache-buster.conf`: ' + err.message);
return process.exit(1);
}
winston.info('[build] Asset compilation successful. Completed in ' + time + 's.');
var time = (Date.now() - buildStart) / 1000;
if (typeof callback === 'function') {
callback();
} else {
process.exit(0);
}
winston.info('[build] Asset compilation successful. Completed in ' + time + 's.');
if (typeof callback === 'function') {
callback();
} else {
process.exit(0);
}
});
});
};

46
src/meta/cacheBuster.js Normal file
View File

@@ -0,0 +1,46 @@
'use strict';
var fs = require('fs');
var path = require('path');
var mkdirp = require('mkdirp');
var winston = require('winston');
var filePath = path.join(__dirname, '../../build/cache-buster');
var cached;
// cache buster is an 11-character, lowercase, alphanumeric string
function generate() {
return (Math.random() * 1e18).toString(32).slice(0, 11);
}
exports.write = function write(callback) {
mkdirp(path.dirname(filePath), function (err) {
if (err) {
return callback(err);
}
fs.writeFile(filePath, generate(), callback);
});
};
exports.read = function read(callback) {
if (cached) {
return callback(null, cached);
}
fs.readFile(filePath, function (err, buffer) {
if (err) {
winston.warn('[cache-buster] could not read cache buster: ' + err.message);
return callback();
}
buffer = buffer.toString();
if (buffer) {
cached = buffer;
return callback(null, cached);
}
callback();
});
};

View File

@@ -6,7 +6,7 @@ var nconf = require('nconf');
var db = require('../database');
var pubsub = require('../pubsub');
var utils = require('../../public/src/utils');
var cacheBuster = require('./cacheBuster');
module.exports = function (Meta) {
@@ -21,10 +21,16 @@ module.exports = function (Meta) {
Meta.configs.list(next);
},
function (config, next) {
config['cache-buster'] = 'v=' + utils.generateUUID();
cacheBuster.read(function (err, buster) {
if (err) {
return next(err);
}
config['cache-buster'] = 'v=' + (buster || Date.now());
Meta.config = config;
setImmediate(next);
Meta.config = config;
next();
});
}
], callback);
};

View File

@@ -18,8 +18,33 @@ var utils = require('../../public/src/utils');
module.exports = function (Meta) {
Meta.css = {};
Meta.css.cache = undefined;
Meta.css.acpCache = undefined;
var buildImports = {
client: function (source) {
return '@import "./theme";\n' + source + '\n' + [
'@import "font-awesome";',
'@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";',
'@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";',
'@import (inline) "../public/vendor/colorpicker/colorpicker.css";',
'@import (inline) "../node_modules/cropperjs/dist/cropper.css";',
'@import "../../public/less/flags.less";',
'@import "../../public/less/blacklist.less";',
'@import "../../public/less/generics.less";',
'@import "../../public/less/mixins.less";',
'@import "../../public/less/global.less";',
].map(function (str) { return str.replace(/\//g, path.sep); }).join('\n');
},
admin: function (source) {
return source + '\n' + [
'@import "font-awesome";',
'@import "../public/less/admin/admin";',
'@import "../public/less/generics.less";',
'@import (inline) "../public/vendor/colorpicker/colorpicker.css";',
'@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";',
'@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";',
].map(function (str) { return str.replace(/\//g, path.sep); }).join('\n');
},
};
Meta.css.minify = function (target, callback) {
callback = callback || function () {};
@@ -30,25 +55,25 @@ module.exports = function (Meta) {
return callback(err);
}
var themeId = (themeData['theme:id'] || 'nodebb-theme-persona'),
baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla')),
paths = [
baseThemePath,
path.join(__dirname, '../../node_modules'),
path.join(__dirname, '../../public/vendor/fontawesome/less')
],
source = '@import "font-awesome";';
var themeId = (themeData['theme:id'] || 'nodebb-theme-persona');
var baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla'));
var paths = [
baseThemePath,
path.join(__dirname, '../../node_modules'),
path.join(__dirname, '../../public/vendor/fontawesome/less')
];
var source = '';
plugins.lessFiles = filterMissingFiles(plugins.lessFiles);
plugins.cssFiles = filterMissingFiles(plugins.cssFiles);
var lessFiles = filterMissingFiles(plugins.lessFiles);
var cssFiles = filterMissingFiles(plugins.cssFiles);
async.waterfall([
function (next) {
getStyleSource(plugins.cssFiles, '\n@import (inline) ".', '.css', next);
getStyleSource(cssFiles, '\n@import (inline) ".', '.css', next);
},
function (src, next) {
source += src;
getStyleSource(plugins.lessFiles, '\n@import ".', '.less', next);
getStyleSource(lessFiles, '\n@import ".', '.less', next);
},
function (src, next) {
source += src;
@@ -59,43 +84,11 @@ module.exports = function (Meta) {
return callback(err);
}
var acpSource = source;
if (target !== 'admin.css') {
source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";';
source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";';
source += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/flags.less";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/blacklist.less";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/generics.less";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/mixins.less";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/global.less";';
source += '\n@import (inline) "..' + path.sep + 'node_modules/cropperjs/dist/cropper.css";';
source = '@import "./theme";\n' + source;
minify(source, paths, 'cache', callback);
} else {
acpSource += '\n@import "..' + path.sep + 'public/less/admin/admin";\n';
acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n';
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n';
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";';
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";';
minify(acpSource, paths, 'acpCache', callback);
}
minify(buildImports[target](source), paths, target, callback);
});
});
};
Meta.css.getFromFile = function (callback) {
async.series([
async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/stylesheet.css'), 'cache'),
async.apply(Meta.css.loadFile, path.join(__dirname, '../../public/admin.css'), 'acpCache')
], function (err) {
callback(err);
});
};
function getStyleSource(files, prefix, extension, callback) {
var pluginDirectories = [],
source = '';
@@ -125,35 +118,22 @@ module.exports = function (Meta) {
});
}
Meta.css.commitToFile = function (filename, callback) {
var file = (filename === 'acpCache' ? 'admin' : 'stylesheet') + '.css';
Meta.css.commitToFile = function (target, source, callback) {
var filename = (target === 'client' ? 'stylesheet' : 'admin') + '.css';
fs.writeFile(path.join(__dirname, '../../public/' + file), Meta.css[filename], function (err) {
fs.writeFile(path.join(__dirname, '../../build/public/' + filename), source, function (err) {
if (!err) {
winston.verbose('[meta/css] ' + file + ' committed to disk.');
winston.verbose('[meta/css] ' + target + ' CSS committed to disk.');
} else {
winston.error('[meta/css] ' + err.message);
process.exit(0);
process.exit(1);
}
callback();
});
};
Meta.css.loadFile = function (filePath, filename, callback) {
winston.verbose('[meta/css] Reading stylesheet ' + filePath.split('/').pop() + ' from file');
fs.readFile(filePath, function (err, file) {
if (err) {
return callback(err);
}
Meta.css[filename] = file;
callback();
});
};
function minify(source, paths, destination, callback) {
function minify(source, paths, target, callback) {
callback = callback || function () {};
less.render(source, {
paths: paths
@@ -163,20 +143,14 @@ module.exports = function (Meta) {
return callback(err);
}
postcss([ autoprefixer, clean() ]).process(lessOutput.css).then(function (result) {
postcss(global.env === 'development' ? [ autoprefixer ] : [ autoprefixer, clean() ]).process(lessOutput.css).then(function (result) {
result.warnings().forEach(function (warn) {
winston.verbose(warn.toString());
});
Meta.css[destination] = result.css;
// Save the compiled CSS in public/ so things like nginx can serve it
if (nconf.get('local-assets') === undefined || nconf.get('local-assets') !== false) {
return Meta.css.commitToFile(destination, function () {
callback(null, result.css);
});
}
callback(null, result.css);
return Meta.css.commitToFile(target, result.css, function () {
callback(null, result.css);
});
});
});
}

View File

@@ -19,14 +19,17 @@ module.exports = function (Meta) {
winston.verbose('Checking dependencies for outdated modules');
async.every(modules, function (module, next) {
async.each(modules, function (module, next) {
fs.readFile(path.join(__dirname, '../../node_modules/', module, 'package.json'), {
encoding: 'utf-8'
}, function (err, pkgData) {
// If a bundled plugin/theme is not present, skip the dep check (#3384)
if (err && err.code === 'ENOENT' && (module === 'nodebb-rewards-essentials' || module.startsWith('nodebb-plugin') || module.startsWith('nodebb-theme'))) {
winston.warn('[meta/dependencies] Bundled plugin ' + module + ' not found, skipping dependency check.');
return next(true);
if (err) {
// If a bundled plugin/theme is not present, skip the dep check (#3384)
if (err.code === 'ENOENT' && (module === 'nodebb-rewards-essentials' || module.startsWith('nodebb-plugin') || module.startsWith('nodebb-theme'))) {
winston.warn('[meta/dependencies] Bundled plugin ' + module + ' not found, skipping dependency check.');
return next();
}
return next(err);
}
try {
@@ -34,20 +37,24 @@ module.exports = function (Meta) {
} catch(e) {
process.stdout.write('[' + 'missing'.red + '] ' + module.bold + ' is a required dependency but could not be found\n');
depsMissing = true;
return next(true);
return next();
}
var ok = !semver.validRange(pkg.dependencies[module]) || semver.satisfies(pkgData.version, pkg.dependencies[module]);
if (ok || (pkgData._resolved && pkgData._resolved.indexOf('//github.com') !== -1)) {
next(true);
next();
} else {
process.stdout.write('[' + 'outdated'.yellow + '] ' + module.bold + ' installed v' + pkgData.version + ', package.json requires ' + pkg.dependencies[module] + '\n');
depsOutdated = true;
next(true);
next();
}
});
}, function (ok) {
}, function (err) {
if (err) {
return callback(err);
}
if (depsMissing) {
callback(new Error('dependencies-missing'));
} else if (depsOutdated) {

View File

@@ -4,8 +4,11 @@ var winston = require('winston');
var fork = require('child_process').fork;
var path = require('path');
var async = require('async');
var nconf = require('nconf');
var fs = require('fs');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');
var file = require('../file');
var plugins = require('../plugins');
var utils = require('../../public/src/utils');
@@ -50,7 +53,6 @@ module.exports = function (Meta) {
'public/src/client/unread.js',
'public/src/client/topic.js',
'public/src/client/topic/events.js',
'public/src/client/topic/flag.js',
'public/src/client/topic/fork.js',
'public/src/client/topic/move.js',
'public/src/client/topic/posts.js',
@@ -73,7 +75,8 @@ module.exports = function (Meta) {
'public/src/modules/taskbar.js',
'public/src/modules/helpers.js',
'public/src/modules/sounds.js',
'public/src/modules/string.js'
'public/src/modules/string.js',
'public/src/modules/flags.js'
],
// modules listed below are routed through express (/src/modules) so they can be defined anonymously
@@ -86,30 +89,45 @@ module.exports = function (Meta) {
}
}
};
Meta.js.bridgeModules = function (app, callback) {
// Add routes for AMD-type modules to serve those files
function addRoute(relPath) {
var relativePath = nconf.get('relative_path');
app.get(relativePath + '/src/modules/' + relPath, function (req, res) {
return res.sendFile(path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]), {
maxAge: app.enabled('cache') ? 5184000000 : 0
});
});
}
var numBridged = 0;
for(var relPath in Meta.js.scripts.modules) {
if (Meta.js.scripts.modules.hasOwnProperty(relPath)) {
addRoute(relPath);
++numBridged;
Meta.js.linkModules = function (callback) {
rimraf(path.join(__dirname, '../../build/public/src/modules'), function (err) {
if (err) {
return callback(err);
}
}
async.eachLimit(Object.keys(Meta.js.scripts.modules), 1000, function (relPath, next) {
var filePath = path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]);
var destPath = path.join(__dirname, '../../build/public/src/modules', relPath);
winston.verbose('[meta/js] ' + numBridged + ' of ' + Object.keys(Meta.js.scripts.modules).length + ' modules bridged');
callback();
mkdirp(path.dirname(destPath), function (err) {
if (err) {
return next(err);
}
file.link(filePath, destPath, next);
});
}, callback);
});
};
Meta.js.linkStatics = function (callback) {
rimraf(path.join(__dirname, '../../build/public/plugins'), function (err) {
if (err) {
return callback(err);
}
async.eachLimit(Object.keys(plugins.staticDirs), 1000, function (mappedPath, next) {
var sourceDir = plugins.staticDirs[mappedPath];
var destDir = path.join(__dirname, '../../build/public/plugins', mappedPath);
mkdirp(path.dirname(destDir), function (err) {
if (err) {
return next(err);
}
file.linkDirs(sourceDir, destDir, next);
});
}, callback);
});
};
Meta.js.minify = function (target, callback) {
@@ -139,12 +157,7 @@ module.exports = function (Meta) {
winston.verbose('[meta/js] ' + target + ' minification complete');
minifier.kill();
if (nconf.get('local-assets') === undefined || nconf.get('local-assets') !== false) {
return Meta.js.commitToFile(target, callback);
} else {
return callback();
}
Meta.js.commitToFile(target, callback);
break;
case 'error':
winston.error('[meta/js] Could not compile ' + target + ': ' + message.message);
@@ -203,48 +216,11 @@ module.exports = function (Meta) {
};
Meta.js.commitToFile = function (target, callback) {
fs.writeFile(path.join(__dirname, '../../public/' + target), Meta.js.target[target].cache, function (err) {
fs.writeFile(path.join(__dirname, '../../build/public/' + target), Meta.js.target[target].cache, function (err) {
callback(err);
});
};
Meta.js.getFromFile = function (target, callback) {
function readFile(filePath, next) {
fs.readFile(filePath, function (err, contents) {
if (err) {
if (err.code === 'ENOENT') {
if (!filePath.endsWith('.map')) {
winston.warn('[meta/js] ' + filePath + ' not found on disk, did you run ./nodebb build?');
}
return next(null, '');
}
}
next(err, contents);
});
}
var scriptPath = path.join(nconf.get('base_dir'), 'public/' + target);
var mapPath = path.join(nconf.get('base_dir'), 'public/' + target + '.map');
async.parallel({
script: function (next) {
readFile(scriptPath, next);
},
map: function (next) {
readFile(mapPath, next);
}
}, function (err, results) {
if (err) {
return callback(err);
}
Meta.js.target[target] = {
cache: results.script,
map: results.map
};
callback();
});
};
function setupDebugging() {
/**
* Check if the parent process is running with the debug option --debug (or --debug-brk)

View File

@@ -28,9 +28,7 @@ function getTranslationTree(callback) {
});
// Filter out plugins with invalid paths
async.filter(paths, file.exists, function (paths) {
next(null, paths);
});
async.filter(paths, file.exists, next);
},
function (paths, next) {
async.map(paths, Plugins.loadPluginInfo, next);

View File

@@ -8,6 +8,7 @@ var rimraf = require('rimraf');
var mkdirp = require('mkdirp');
var async = require('async');
var file = require('../file');
var plugins = require('../plugins');
var db = require('../database');
@@ -28,7 +29,7 @@ module.exports = function (Meta) {
Meta.sounds.getFiles = function (callback) {
async.waterfall([
function (next) {
fs.readdir(path.join(__dirname, '../../public/sounds'), next);
fs.readdir(path.join(__dirname, '../../build/public/sounds'), next);
},
function (sounds, next) {
fs.readdir(path.join(__dirname, '../../public/uploads/sounds'), function (err, uploaded) {
@@ -88,7 +89,7 @@ module.exports = function (Meta) {
};
function setupSounds(callback) {
var soundsPath = path.join(__dirname, '../../public/sounds');
var soundsPath = path.join(__dirname, '../../build/public/sounds');
async.waterfall([
function (next) {
@@ -107,21 +108,6 @@ module.exports = function (Meta) {
return;
}
if (nconf.get('local-assets') === false) {
// Don't regenerate the public/sounds/ directory. Instead, create a mapping for the router to use
Meta.sounds._filePathHash = filePaths.reduce(function (hash, filePath) {
hash[path.basename(filePath)] = filePath;
return hash;
}, {});
winston.verbose('[sounds] Sounds OK');
if (typeof next === 'function') {
return next();
} else {
return;
}
}
// Clear the sounds directory
async.series([
function (next) {
@@ -138,11 +124,7 @@ module.exports = function (Meta) {
// 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);
}
file.link(filePath, path.join(soundsPath, path.basename(filePath)), next);
}, function (err) {
if (!err) {
winston.verbose('[sounds] Sounds OK');

View File

@@ -27,18 +27,28 @@ module.exports = function (Meta) {
async.filter(files, function (file, next) {
fs.stat(path.join(themePath, file), function (err, fileStat) {
if (err) {
return next(false);
if (err.code === 'ENOENT') {
return next(null, false);
}
return next(err);
}
next((fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-'));
next(null, (fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-'));
});
}, function (themes) {
}, function (err, themes) {
if (err) {
return callback(err);
}
async.map(themes, function (theme, next) {
var config = path.join(themePath, theme, 'theme.json');
fs.readFile(config, function (err, file) {
if (err) {
return next();
if (err.code === 'ENOENT') {
return next(null, null);
}
return next(err);
}
try {
var configObj = JSON.parse(file.toString());
@@ -48,7 +58,7 @@ module.exports = function (Meta) {
if (configObj.screenshot) {
configObj.screenshot_url = nconf.get('relative_path') + '/css/previews/' + configObj.id;
} else {
configObj.screenshot_url = nconf.get('relative_path') + '/images/themes/default.png';
configObj.screenshot_url = nconf.get('relative_path') + '/assets/images/themes/default.png';
}
next(null, configObj);
} catch (err) {

View File

@@ -101,7 +101,7 @@ module.exports = function (middleware) {
plugins: results.custom_header.plugins,
authentication: results.custom_header.authentication,
scripts: results.scripts,
'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '',
'cache-buster': meta.config['cache-buster'] || '',
env: process.env.NODE_ENV ? true : false,
title: (acpPath || 'Dashboard') + ' | NodeBB Admin Control Panel',
bodyClass: data.bodyClass

View File

@@ -46,7 +46,7 @@ module.exports = function (middleware) {
bootswatchCSS: meta.config['theme:src'],
title: meta.config.title || '',
description: meta.config.description || '',
'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '',
'cache-buster': meta.config['cache-buster'] || '',
'brand:logo': meta.config['brand:logo'] || '',
'brand:logo:url': meta.config['brand:logo:url'] || '',
'brand:logo:alt': meta.config['brand:logo:alt'] || '',

View File

@@ -73,6 +73,30 @@ middleware.ensureSelfOrGlobalPrivilege = function (req, res, next) {
}
};
middleware.ensureSelfOrPrivileged = function (req, res, next) {
/*
The "self" part of this middleware hinges on you having used
middleware.exposeUid prior to invoking this middleware.
*/
if (req.user) {
if (parseInt(req.user.uid, 10) === parseInt(res.locals.uid, 10)) {
return next();
}
user.isPrivileged(req.uid, function (err, ok) {
if (err) {
return next(err);
} else if (ok) {
return next();
} else {
controllers.helpers.notAllowed(req, res);
}
});
} else {
controllers.helpers.notAllowed(req, res);
}
};
middleware.pageView = function (req, res, next) {
analytics.pageView({
ip: req.ip,

View File

@@ -415,6 +415,7 @@ var utils = require('../public/src/utils');
'notifications:user_started_following_you',
'notifications:user_posted_to',
'notifications:user_flagged_post_in',
'notifications:user_flagged_user',
'new_register'
],
isolated, differentiators, differentiator, modifyIndex, set;
@@ -462,6 +463,7 @@ var utils = require('../public/src/utils');
case 'notifications:user_started_following_you':
case 'notifications:user_posted_to':
case 'notifications:user_flagged_post_in':
case 'notifications:user_flagged_user':
var usernames = set.map(function (notifObj) {
return notifObj && notifObj.user && notifObj.user.username;
}).filter(function (username, idx, array) {

View File

@@ -159,9 +159,7 @@ var middleware;
});
// Filter out plugins with invalid paths
async.filter(paths, file.exists, function (paths) {
next(null, paths);
});
async.filter(paths, file.exists, next);
},
function (paths, next) {
async.map(paths, Plugins.loadPluginInfo, next);
@@ -344,11 +342,15 @@ var middleware;
async.filter(dirs, function (dir, callback) {
fs.stat(dir, function (err, stats) {
callback(!err && stats.isDirectory());
if (err) {
if (err.code === 'ENOENT') {
return callback(null, false);
}
return callback(err);
}
callback(null, stats.isDirectory());
});
}, function (plugins) {
next(null, plugins);
});
}, next);
},
function (files, next) {

View File

@@ -7,7 +7,10 @@ module.exports = function (Plugins) {
Plugins.deprecatedHooks = {
'filter:user.custom_fields': null, // remove in v1.1.0
'filter:post.save': 'filter:post.create',
'filter:user.profileLinks': 'filter:user.profileMenu'
'filter:user.profileLinks': 'filter:user.profileMenu',
'action:post.flag': 'action:flag.create',
'action:plugin.activate': null,
'action:plugin.install': null
};
/*
`data` is an object consisting of (* is required):

View File

@@ -50,8 +50,11 @@ module.exports = function (Plugins) {
},
function (next) {
meta.reloadRequired = true;
Plugins.fireHook(isActive ? 'action:plugin.deactivate' : 'action:plugin.activate', id);
next();
if (isActive) {
Plugins.fireHook('action:plugin.deactivate', {id: id});
}
setImmediate(next);
}
], function (err) {
if (err) {
@@ -68,7 +71,6 @@ module.exports = function (Plugins) {
};
function toggleInstall(id, version, callback) {
var type;
var installed;
async.waterfall([
function (next) {
@@ -76,7 +78,6 @@ module.exports = function (Plugins) {
},
function (_installed, next) {
installed = _installed;
type = installed ? 'uninstall' : 'install';
Plugins.isActive(id, next);
},
function (active, next) {
@@ -86,23 +87,26 @@ module.exports = function (Plugins) {
});
return;
}
next();
setImmediate(next);
},
function (next) {
runNpmCommand(type, id, version || 'latest', next);
runNpmCommand(installed ? 'uninstall' : 'install', id, version || 'latest', next);
},
function (next) {
Plugins.get(id, next);
},
function (pluginData, next) {
Plugins.fireHook('action:plugin.' + type, id);
next(null, pluginData);
if (installed) {
Plugins.fireHook('action:plugin.uninstall', {id: id, version: version});
}
setImmediate(next, null, pluginData);
}
], callback);
}
function runNpmCommand(command, pkgName, version, callback) {
require('child_process').execFile('npm', [command, pkgName + (command === 'install' ? '@' + version : '')], function (err, stdout) {
require('child_process').execFile((process.platform === 'win32') ? 'npm.cmd' : 'npm', [command, pkgName + (command === 'install' ? '@' + version : '')], function (err, stdout) {
if (err) {
return callback(err);
}

View File

@@ -29,9 +29,7 @@ module.exports = function (Plugins) {
return path.join(__dirname, '../../node_modules/', plugin);
});
async.filter(plugins, file.exists, function (plugins) {
next(null, plugins);
});
async.filter(plugins, file.exists, next);
},
], callback);
};
@@ -54,7 +52,9 @@ module.exports = function (Plugins) {
async.parallel([
async.apply(mapFiles, pluginData, 'css', 'cssFiles'),
async.apply(mapFiles, pluginData, 'less', 'lessFiles'),
async.apply(mapClientSideScripts, pluginData)
async.apply(mapClientSideScripts, pluginData),
async.apply(mapClientModules, pluginData),
async.apply(mapStaticDirectories, pluginData, pluginData.path),
], next);
}, next);
}
@@ -90,7 +90,7 @@ module.exports = function (Plugins) {
},
function (next) {
mapClientModules(pluginData, next);
},
}
], function (err) {
if (err) {
winston.verbose('[plugins] Could not load plugin : ' + pluginData.id);
@@ -157,13 +157,13 @@ module.exports = function (Plugins) {
var realPath = pluginData.staticDirs[mappedPath];
var staticDir = path.join(pluginPath, realPath);
file.exists(staticDir, function (exists) {
file.exists(staticDir, function (err, exists) {
if (exists) {
Plugins.staticDirs[pluginData.id + '/' + mappedPath] = staticDir;
} else {
winston.warn('[plugins/' + pluginData.id + '] Mapped path \'' + mappedPath + ' => ' + staticDir + '\' not found.');
}
callback();
callback(err);
});
}
}
@@ -298,6 +298,7 @@ module.exports = function (Plugins) {
pluginData.version = packageData.version;
pluginData.repository = packageData.repository;
pluginData.nbbpm = packageData.nbbpm;
pluginData.path = pluginPath;
} catch(err) {
var pluginDir = pluginPath.split(path.sep);
pluginDir = pluginDir[pluginDir.length - 1];

View File

@@ -21,7 +21,6 @@ var plugins = require('./plugins');
require('./posts/category')(Posts);
require('./posts/summary')(Posts);
require('./posts/recent')(Posts);
require('./posts/flags')(Posts);
require('./posts/tools')(Posts);
require('./posts/votes')(Posts);
require('./posts/bookmarks')(Posts);
@@ -157,7 +156,7 @@ var plugins = require('./plugins');
pid: pid
};
data[field] = value;
plugins.fireHook('action:post.setFields', data);
plugins.fireHook('action:post.setFields', {data: data});
callback();
});
};
@@ -168,7 +167,7 @@ var plugins = require('./plugins');
return callback(err);
}
data.pid = pid;
plugins.fireHook('action:post.setFields', data);
plugins.fireHook('action:post.setFields', {data: data});
callback();
});
};

View File

@@ -108,7 +108,7 @@ module.exports = function (Posts) {
},
function (postData, next) {
postData.isMain = isMain;
plugins.fireHook('action:post.save', _.clone(postData));
plugins.fireHook('action:post.save', {post: _.clone(postData)});
next(null, postData);
}
], callback);

View File

@@ -8,6 +8,7 @@ var topics = require('../topics');
var user = require('../user');
var notifications = require('../notifications');
var plugins = require('../plugins');
var flags = require('../flags');
module.exports = function (Posts) {
@@ -28,6 +29,7 @@ module.exports = function (Posts) {
topics.getTopicFields(_post.tid, ['tid', 'cid', 'pinned'], next);
},
function (topicData, next) {
postData.cid = topicData.cid;
async.parallel([
function (next) {
updateTopicTimestamp(topicData, next);
@@ -41,7 +43,7 @@ module.exports = function (Posts) {
], next);
},
function (results, next) {
plugins.fireHook('action:post.delete', pid);
plugins.fireHook('action:post.delete', {post: _.clone(postData), uid: uid});
next(null, postData);
}
], callback);
@@ -78,7 +80,7 @@ module.exports = function (Posts) {
], next);
},
function (results, next) {
plugins.fireHook('action:post.restore', _.clone(postData));
plugins.fireHook('action:post.restore', {post: _.clone(postData), uid: uid});
next(null, postData);
}
], callback);
@@ -143,17 +145,17 @@ module.exports = function (Posts) {
},
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);
next(err);
});
},
function (next) {
Posts.getPostData(pid, next);
},
function (postData, next) {
plugins.fireHook('action:post.purge', {post: postData, uid: uid});
db.delete('post:' + pid, next);
}
], callback);
};

View File

@@ -66,7 +66,7 @@ module.exports = function (Posts) {
postData.cid = results.topic.cid;
postData.topic = results.topic;
plugins.fireHook('action:post.edit', _.clone(postData));
plugins.fireHook('action:post.edit', {post: _.clone(postData), uid: data.uid});
cache.del(String(postData.pid));
pubsub.publish('post:edit', String(postData.pid));
@@ -137,7 +137,7 @@ module.exports = function (Posts) {
function (tags, next) {
topicData.tags = data.tags;
topicData.oldTitle = results.topic.title;
plugins.fireHook('action:topic.edit', topicData);
plugins.fireHook('action:topic.edit', {topic: topicData, uid: data.uid});
next(null, {
tid: tid,
cid: results.topic.cid,

View File

@@ -1,417 +0,0 @@
'use strict';
var async = require('async');
var winston = require('winston');
var db = require('../database');
var user = require('../user');
var analytics = require('../analytics');
module.exports = function (Posts) {
Posts.flag = function (post, uid, reason, callback) {
if (!parseInt(uid, 10) || !reason) {
return callback();
}
async.waterfall([
function (next) {
async.parallel({
hasFlagged: async.apply(Posts.isFlaggedByUser, post.pid, uid),
exists: async.apply(Posts.exists, post.pid)
}, next);
},
function (results, next) {
if (!results.exists) {
return next(new Error('[[error:no-post]]'));
}
if (results.hasFlagged) {
return next(new Error('[[error:already-flagged]]'));
}
var now = Date.now();
async.parallel([
function (next) {
db.sortedSetAdd('posts:flagged', now, post.pid, next);
},
function (next) {
db.sortedSetIncrBy('posts:flags:count', 1, post.pid, next);
},
function (next) {
db.incrObjectField('post:' + post.pid, 'flags', next);
},
function (next) {
db.sortedSetAdd('pid:' + post.pid + ':flag:uids', now, uid, next);
},
function (next) {
db.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, next);
},
function (next) {
if (parseInt(post.uid, 10)) {
async.parallel([
async.apply(db.sortedSetIncrBy, 'users:flags', 1, post.uid),
async.apply(db.incrObjectField, 'user:' + post.uid, 'flags'),
async.apply(db.sortedSetAdd, 'uid:' + post.uid + ':flag:pids', now, post.pid)
], next);
} else {
next();
}
}
], next);
},
function (data, next) {
openNewFlag(post.pid, uid, next);
}
], function (err) {
if (err) {
return callback(err);
}
analytics.increment('flags');
callback();
});
};
function openNewFlag(pid, uid, callback) {
db.sortedSetScore('posts:flags:count', pid, function (err, count) {
if (err) {
return callback(err);
}
if (count === 1) { // Only update state on new flag
Posts.updateFlagData(uid, pid, {
state: 'open'
}, callback);
} else {
callback();
}
});
}
Posts.isFlaggedByUser = function (pid, uid, callback) {
db.isSortedSetMember('pid:' + pid + ':flag:uids', uid, callback);
};
Posts.dismissFlag = function (pid, callback) {
async.waterfall([
function (next) {
db.getObjectFields('post:' + pid, ['pid', 'uid', 'flags'], next);
},
function (postData, next) {
if (!postData.pid) {
return callback();
}
async.parallel([
function (next) {
if (parseInt(postData.uid, 10)) {
if (parseInt(postData.flags, 10) > 0) {
async.parallel([
async.apply(db.sortedSetIncrBy, 'users:flags', -postData.flags, postData.uid),
async.apply(db.incrObjectFieldBy, 'user:' + postData.uid, 'flags', -postData.flags)
], next);
} else {
next();
}
} else {
next();
}
},
function (next) {
db.sortedSetsRemove([
'posts:flagged',
'posts:flags:count',
'uid:' + postData.uid + ':flag:pids'
], pid, next);
},
function (next) {
async.series([
function (next) {
db.getSortedSetRange('pid:' + pid + ':flag:uids', 0, -1, function (err, uids) {
if (err) {
return next(err);
}
async.each(uids, function (uid, next) {
var nid = 'post_flag:' + pid + ':uid:' + uid;
async.parallel([
async.apply(db.delete, 'notifications:' + nid),
async.apply(db.sortedSetRemove, 'notifications', 'post_flag:' + pid + ':uid:' + uid)
], next);
}, next);
});
},
async.apply(db.delete, 'pid:' + pid + ':flag:uids')
], next);
},
async.apply(db.deleteObjectField, 'post:' + pid, 'flags'),
async.apply(db.delete, 'pid:' + pid + ':flag:uid:reason'),
async.apply(db.deleteObjectFields, 'post:' + pid, ['flag:state', 'flag:assignee', 'flag:notes', 'flag:history'])
], next);
},
function (results, next) {
db.sortedSetsRemoveRangeByScore(['users:flags'], '-inf', 0, next);
}
], callback);
};
Posts.dismissAllFlags = function (callback) {
db.getSortedSetRange('posts:flagged', 0, -1, function (err, pids) {
if (err) {
return callback(err);
}
async.eachSeries(pids, Posts.dismissFlag, callback);
});
};
Posts.dismissUserFlags = function (uid, callback) {
db.getSortedSetRange('uid:' + uid + ':flag:pids', 0, -1, function (err, pids) {
if (err) {
return callback(err);
}
async.eachSeries(pids, Posts.dismissFlag, callback);
});
};
Posts.getFlags = function (set, cid, uid, start, stop, callback) {
async.waterfall([
function (next) {
if (Array.isArray(set)) {
db.getSortedSetRevIntersect({sets: set, start: start, stop: -1, aggregate: 'MAX'}, next);
} else {
db.getSortedSetRevRange(set, start, -1, next);
}
},
function (pids, next) {
if (cid) {
Posts.filterPidsByCid(pids, cid, next);
} else {
process.nextTick(next, null, pids);
}
},
function (pids, next) {
getFlaggedPostsWithReasons(pids, uid, next);
},
function (posts, next) {
var count = posts.length;
var end = stop - start + 1;
next(null, {posts: posts.slice(0, stop === -1 ? undefined : end), count: count});
}
], callback);
};
function getFlaggedPostsWithReasons(pids, uid, callback) {
async.waterfall([
function (next) {
async.parallel({
uidsReasons: function (next) {
async.map(pids, function (pid, next) {
db.getSortedSetRange('pid:' + pid + ':flag:uid:reason', 0, -1, next);
}, next);
},
posts: function (next) {
Posts.getPostSummaryByPids(pids, uid, {stripTags: false, extraFields: ['flags', 'flag:assignee', 'flag:state', 'flag:notes', 'flag:history']}, next);
}
}, next);
},
function (results, next) {
async.map(results.uidsReasons, function (uidReasons, next) {
async.map(uidReasons, function (uidReason, next) {
var uid = uidReason.split(':')[0];
var reason = uidReason.substr(uidReason.indexOf(':') + 1);
user.getUserFields(uid, ['username', 'userslug', 'picture'], function (err, userData) {
next(err, {user: userData, reason: reason});
});
}, next);
}, function (err, reasons) {
if (err) {
return callback(err);
}
results.posts.forEach(function (post, index) {
if (post) {
post.flagReasons = reasons[index];
}
});
next(null, results.posts);
});
},
async.apply(Posts.expandFlagHistory),
function (posts, next) {
// Parse out flag data into its own object inside each post hash
async.map(posts, function (postObj, next) {
for(var prop in postObj) {
postObj.flagData = postObj.flagData || {};
if (postObj.hasOwnProperty(prop) && prop.startsWith('flag:')) {
postObj.flagData[prop.slice(5)] = postObj[prop];
if (prop === 'flag:state') {
switch(postObj[prop]) {
case 'open':
postObj.flagData.labelClass = 'info';
break;
case 'wip':
postObj.flagData.labelClass = 'warning';
break;
case 'resolved':
postObj.flagData.labelClass = 'success';
break;
case 'rejected':
postObj.flagData.labelClass = 'danger';
break;
}
}
delete postObj[prop];
}
}
if (postObj.flagData.assignee) {
user.getUserFields(parseInt(postObj.flagData.assignee, 10), ['username', 'picture'], function (err, userData) {
if (err) {
return next(err);
}
postObj.flagData.assigneeUser = userData;
next(null, postObj);
});
} else {
setImmediate(next.bind(null, null, postObj));
}
}, next);
}
], callback);
}
Posts.updateFlagData = function (uid, pid, flagObj, callback) {
// Retrieve existing flag data to compare for history-saving purposes
var changes = [];
var changeset = {};
var prop;
Posts.getPostData(pid, function (err, postData) {
if (err) {
return callback(err);
}
// Track new additions
for(prop in flagObj) {
if (flagObj.hasOwnProperty(prop) && !postData.hasOwnProperty('flag:' + prop) && flagObj[prop].length) {
changes.push(prop);
}
}
// Track changed items
for(prop in postData) {
if (
postData.hasOwnProperty(prop) && prop.startsWith('flag:') &&
flagObj.hasOwnProperty(prop.slice(5)) &&
postData[prop] !== flagObj[prop.slice(5)]
) {
changes.push(prop.slice(5));
}
}
changeset = changes.reduce(function (memo, prop) {
memo['flag:' + prop] = flagObj[prop];
return memo;
}, {});
// Append changes to history string
if (changes.length) {
try {
var history = JSON.parse(postData['flag:history'] || '[]');
changes.forEach(function (property) {
switch(property) {
case 'assignee': // intentional fall-through
case 'state':
history.unshift({
uid: uid,
type: property,
value: flagObj[property],
timestamp: Date.now()
});
break;
case 'notes':
history.unshift({
uid: uid,
type: property,
timestamp: Date.now()
});
}
});
changeset['flag:history'] = JSON.stringify(history);
} catch (e) {
winston.warn('[posts/updateFlagData] Unable to deserialise post flag history, likely malformed data');
}
}
// Save flag data into post hash
if (changes.length) {
Posts.setPostFields(pid, changeset, callback);
} else {
setImmediate(callback);
}
});
};
Posts.expandFlagHistory = function (posts, callback) {
// Expand flag history
async.map(posts, function (post, next) {
var history;
try {
history = JSON.parse(post['flag:history'] || '[]');
} catch (e) {
winston.warn('[posts/getFlags] Unable to deserialise post flag history, likely malformed data');
return callback(e);
}
async.map(history, function (event, next) {
event.timestampISO = new Date(event.timestamp).toISOString();
async.parallel([
function (next) {
user.getUserFields(event.uid, ['username', 'picture'], function (err, userData) {
if (err) {
return next(err);
}
event.user = userData;
next();
});
},
function (next) {
if (event.type === 'assignee') {
user.getUserField(parseInt(event.value, 10), 'username', function (err, username) {
if (err) {
return next(err);
}
event.label = username || 'Unknown user';
next(null);
});
} else if (event.type === 'state') {
event.label = '[[topic:flag_manage_state_' + event.value + ']]';
setImmediate(next);
} else {
setImmediate(next);
}
}
], function (err) {
next(err, event);
});
}, function (err, history) {
if (err) {
return next(err);
}
post['flag:history'] = history;
next(null, post);
});
}, callback);
};
};

View File

@@ -8,12 +8,8 @@ var helpers = {};
helpers.some = function (tasks, callback) {
async.some(tasks, function (task, next) {
task(function (err, result) {
next(!err && result);
});
}, function (result) {
callback(null, result);
});
task(next);
}, callback);
};
helpers.isUserAllowedTo = function (privilege, uid, cid, callback) {

View File

@@ -37,12 +37,14 @@ rewards.checkConditionAndRewardUser = function (uid, condition, method, callback
function (rewards, next) {
async.filter(rewards, function (reward, next) {
if (!reward) {
return next(false);
return next(null, false);
}
checkCondition(reward, method, next);
}, function (eligible) {
if (!eligible) {
checkCondition(reward, method, function (result) {
next(null, result);
});
}, function (err, eligible) {
if (err || !eligible) {
return next(false);
}

View File

@@ -56,7 +56,6 @@ function addRoutes(router, middleware, controllers) {
router.get('/manage/categories/:category_id/analytics', middlewares, controllers.admin.categories.getAnalytics);
router.get('/manage/tags', middlewares, controllers.admin.tags.get);
router.get('/manage/flags', middlewares, controllers.admin.flags.get);
router.get('/manage/ip-blacklist', middlewares, controllers.admin.blacklist.get);
router.get('/manage/users', middlewares, controllers.admin.users.sortByJoinDate);

View File

@@ -3,10 +3,16 @@
var helpers = {};
helpers.setupPageRoute = function (router, name, middleware, middlewares, controller) {
middlewares = middlewares.concat([middleware.maintenanceMode, middleware.registrationComplete, middleware.pageView, middleware.pluginHooks]);
middlewares = [middleware.maintenanceMode, middleware.registrationComplete, middleware.pageView, middleware.pluginHooks].concat(middlewares);
router.get(name, middleware.busyCheck, middleware.buildHeader, middlewares, controller);
router.get('/api' + name, middlewares, controller);
};
helpers.setupAdminPageRoute = function (router, name, middleware, middlewares, controller) {
router.get(name, middleware.admin.buildHeader, middlewares, controller);
router.get('/api' + name, middlewares, controller);
};
module.exports = helpers;

View File

@@ -15,7 +15,6 @@ var metaRoutes = require('./meta');
var apiRoutes = require('./api');
var adminRoutes = require('./admin');
var feedRoutes = require('./feeds');
var pluginRoutes = require('./plugins');
var authRoutes = require('./authentication');
var helpers = require('./helpers');
@@ -41,7 +40,8 @@ function mainRoutes(app, middleware, controllers) {
}
function modRoutes(app, middleware, controllers) {
setupPageRoute(app, '/posts/flags', middleware, [], controllers.mods.flagged);
setupPageRoute(app, '/flags', middleware, [], controllers.mods.flags.list);
setupPageRoute(app, '/flags/:flagId', middleware, [], controllers.mods.flags.detail);
}
function globalModRoutes(app, middleware, controllers) {
@@ -121,7 +121,6 @@ module.exports = function (app, middleware, hotswapIds) {
metaRoutes(router, middleware, controllers);
apiRoutes(router, middleware, controllers);
feedRoutes(router, middleware, controllers);
pluginRoutes(router, middleware, controllers);
mainRoutes(router, middleware, controllers);
topicRoutes(router, middleware, controllers);
@@ -143,10 +142,44 @@ module.exports = function (app, middleware, hotswapIds) {
}
app.use(middleware.privateUploads);
app.use(relativePath + '/assets', express.static(path.join(__dirname, '../../', 'build/public'), {
app.use(relativePath + '/assets', express.static(path.join(__dirname, '../../build/public'), {
maxAge: app.enabled('cache') ? 5184000000 : 0
}));
app.use(relativePath + '/assets', express.static(path.join(__dirname, '../../public'), {
maxAge: app.enabled('cache') ? 5184000000 : 0
}));
// TODO: deprecate?
app.use(relativePath + '/plugins', express.static(path.join(__dirname, '../../build/public/plugins'), {
maxAge: app.enabled('cache') ? 5184000000 : 0
}));
// DEPRECATED
var deprecatedPaths = [
'/nodebb.min.js',
'/acp.min.js',
'/stylesheet.css',
'/js-enabled.css',
'/admin.css',
'/logo.png',
'/favicon.ico',
'/vendor/',
'/uploads/',
'/templates/',
'/src/',
'/images/',
'/language/',
'/sounds/',
];
app.use(relativePath, function (req, res, next) {
if (deprecatedPaths.some(function (path) { return req.path.startsWith(path); })) {
winston.warn('[deprecated] Accessing `' + req.path.slice(1) + '` from `/` is deprecated. ' +
'Use `/assets' + req.path + '` to access this file.');
res.redirect(relativePath + '/assets' + req.path + '?' + meta.config['cache-buster']);
} else {
next();
}
});
// DEPRECATED
app.use(relativePath + '/api/language', function (req, res) {
winston.warn('[deprecated] Accessing language files from `/api/language` is deprecated. ' +
@@ -154,10 +187,7 @@ module.exports = function (app, middleware, hotswapIds) {
res.redirect(relativePath + '/assets/language' + req.path + '.json?' + meta.config['cache-buster']);
});
app.use(relativePath, express.static(path.join(__dirname, '../../', 'public'), {
maxAge: app.enabled('cache') ? 5184000000 : 0
}));
app.use(relativePath + '/vendor/jquery/timeago/locales', middleware.processTimeagoLocales);
app.use(relativePath + '/assets/vendor/jquery/timeago/locales', middleware.processTimeagoLocales);
app.use(controllers.handle404);
app.use(controllers.handleURIErrors);
app.use(controllers.handleErrors);

View File

@@ -1,51 +1,6 @@
"use strict";
var path = require('path');
var nconf = require('nconf');
var meta = require('../meta');
function sendMinifiedJS(req, res) {
var target = path.basename(req.path);
var cache = meta.js.target[target] ? meta.js.target[target].cache : '';
res.type('text/javascript').send(cache);
}
// The portions of code involving the source map are commented out as they're broken in UglifyJS2
// Follow along here: https://github.com/mishoo/UglifyJS2/issues/700
// function sendJSSourceMap(req, res) {
// if (meta.js.hasOwnProperty('map')) {
// res.type('application/json').send(meta.js.map);
// } else {
// res.redirect(404);
// }
// };
function sendStylesheet(req, res) {
res.type('text/css').status(200).send(meta.css.cache);
}
function sendACPStylesheet(req, res) {
res.type('text/css').status(200).send(meta.css.acpCache);
}
function sendSoundFile(req, res, next) {
var resolved = meta.sounds._filePathHash[path.basename(req.path)];
if (resolved) {
res.status(200).sendFile(resolved);
} else {
next();
}
}
module.exports = function (app, middleware, controllers) {
app.get('/stylesheet.css', middleware.addExpiresHeaders, sendStylesheet);
app.get('/admin.css', middleware.addExpiresHeaders, sendACPStylesheet);
app.get('/nodebb.min.js', middleware.addExpiresHeaders, sendMinifiedJS);
app.get('/acp.min.js', middleware.addExpiresHeaders, sendMinifiedJS);
// app.get('/nodebb.min.js.map', middleware.addExpiresHeaders, sendJSSourceMap);
app.get('/sitemap.xml', controllers.sitemap.render);
app.get('/sitemap/pages.xml', controllers.sitemap.getPages);
app.get('/sitemap/categories.xml', controllers.sitemap.getCategories);
@@ -53,8 +8,4 @@ module.exports = function (app, middleware, controllers) {
app.get('/robots.txt', controllers.robots);
app.get('/manifest.json', controllers.manifest);
app.get('/css/previews/:theme', controllers.admin.themes.get);
if (nconf.get('local-assets') === false) {
app.get('/sounds/*', middleware.addExpiresHeaders, sendSoundFile);
}
};

View File

@@ -1,40 +0,0 @@
"use strict";
var _ = require('underscore');
var path = require('path');
var plugins = require('../plugins');
module.exports = function (app, middleware, controllers) {
// Static Assets
app.get('/plugins/:id/*', middleware.addExpiresHeaders, function (req, res, next) {
var relPath = req._parsedUrl.pathname.replace('/plugins/', '');
var matches = _.map(plugins.staticDirs, function (realPath, mappedPath) {
if (relPath.match(mappedPath)) {
var pathToFile = path.join(plugins.staticDirs[mappedPath], decodeURIComponent(relPath.slice(mappedPath.length)));
if (pathToFile.startsWith(plugins.staticDirs[mappedPath])) {
return pathToFile;
}
}
return null;
}).filter(Boolean);
if (!matches || !matches.length) {
return next();
}
res.sendFile(matches[0], {}, function (err) {
if (err) {
if (err.code === 'ENOENT') {
// File doesn't exist, this isn't an error, to send to 404 handler
return next();
} else {
return next(err);
}
}
});
});
};

View File

@@ -147,7 +147,7 @@ function getMatchedPosts(pids, data, callback) {
topicFields.push('postcount');
}
if (data.sortBy) {
if (data.sortBy && data.sortBy !== 'relevance') {
if (data.sortBy.startsWith('category')) {
topicFields.push('cid');
} else if (data.sortBy.startsWith('topic.')) {
@@ -328,7 +328,7 @@ function filterByTags(posts, hasTags) {
}
function sortPosts(posts, data) {
if (!posts.length || !data.sortBy) {
if (!posts.length || !data.sortBy || data.sortBy === 'relevance') {
return;
}

View File

@@ -168,10 +168,11 @@ SocketAdmin.config.setMultiple = function (socket, data, callback) {
key: field,
value: data[field]
};
plugins.fireHook('action:config.set', setting);
logger.monitorConfig({io: index.server}, setting);
}
}
plugins.fireHook('action:config.set', {settings: data});
setImmediate(next);
}
], callback);

View File

@@ -69,14 +69,6 @@ User.resetLockouts = function (socket, uids, callback) {
async.each(uids, user.auth.resetLockout, callback);
};
User.resetFlags = function (socket, uids, callback) {
if (!Array.isArray(uids)) {
return callback(new Error('[[error:invalid-data]]'));
}
user.resetFlags(uids, callback);
};
User.validateEmail = function (socket, uids, callback) {
if (!Array.isArray(uids)) {
return callback(new Error('[[error:invalid-data]]'));

111
src/socket.io/flags.js Normal file
View File

@@ -0,0 +1,111 @@
'use strict';
var async = require('async');
var S = require('string');
var user = require('../user');
var groups = require('../groups');
var posts = require('../posts');
var topics = require('../topics');
var privileges = require('../privileges');
var notifications = require('../notifications');
var plugins = require('../plugins');
var meta = require('../meta');
var utils = require('../../public/src/utils');
var flags = require('../flags');
var SocketFlags = {};
SocketFlags.create = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:not-logged-in]]'));
}
if (!data || !data.type || !data.id || !data.reason) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
async.apply(flags.validate, {
uid: socket.uid,
type: data.type,
id: data.id
}),
function (next) {
// If we got here, then no errors occurred
flags.create(data.type, data.id, socket.uid, data.reason, next);
}
], function (err, flagObj) {
if (err) {
return callback(err);
}
flags.notify(flagObj, socket.uid);
callback(null, flagObj);
});
};
SocketFlags.update = function (socket, data, callback) {
if (!data || !(data.flagId && data.data)) {
return callback(new Error('[[error:invalid-data]]'));
}
var payload = {};
async.waterfall([
function (next) {
async.parallel([
async.apply(user.isAdminOrGlobalMod, socket.uid),
async.apply(user.isModeratorOfAnyCategory, socket.uid)
], function (err, results) {
next(err, results[0] || results[1]);
});
},
function (allowed, next) {
if (!allowed) {
return next(new Error('[[no-privileges]]'));
}
// Translate form data into object
payload = data.data.reduce(function (memo, cur) {
memo[cur.name] = cur.value;
return memo;
}, payload);
flags.update(data.flagId, socket.uid, payload, next);
},
async.apply(flags.getHistory, data.flagId)
], callback);
};
SocketFlags.appendNote = function (socket, data, callback) {
if (!data || !(data.flagId && data.note)) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
function (next) {
async.parallel([
async.apply(user.isAdminOrGlobalMod, socket.uid),
async.apply(user.isModeratorOfAnyCategory, socket.uid)
], function (err, results) {
next(err, results[0] || results[1]);
});
},
function (allowed, next) {
if (!allowed) {
return next(new Error('[[no-privileges]]'));
}
flags.appendNote(data.flagId, socket.uid, data.note, next);
},
function (next) {
async.parallel({
"notes": async.apply(flags.getNotes, data.flagId),
"history": async.apply(flags.getHistory, data.flagId)
}, next);
}
], callback);
};
module.exports = SocketFlags;

View File

@@ -130,7 +130,7 @@ function onMessage(socket, payload) {
function requireModules() {
var modules = ['admin', 'categories', 'groups', 'meta', 'modules',
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist'
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist', 'flags'
];
modules.forEach(function (module) {

View File

@@ -20,7 +20,6 @@ require('./posts/move')(SocketPosts);
require('./posts/votes')(SocketPosts);
require('./posts/bookmarks')(SocketPosts);
require('./posts/tools')(SocketPosts);
require('./posts/flag')(SocketPosts);
SocketPosts.reply = function (socket, data, callback) {
if (!data || !data.tid || !data.content) {

View File

@@ -1,172 +0,0 @@
'use strict';
var async = require('async');
var S = require('string');
var user = require('../../user');
var groups = require('../../groups');
var posts = require('../../posts');
var topics = require('../../topics');
var privileges = require('../../privileges');
var notifications = require('../../notifications');
var plugins = require('../../plugins');
var meta = require('../../meta');
var utils = require('../../../public/src/utils');
module.exports = function (SocketPosts) {
SocketPosts.flag = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:not-logged-in]]'));
}
if (!data || !data.pid || !data.reason) {
return callback(new Error('[[error:invalid-data]]'));
}
var flaggingUser = {};
var post;
async.waterfall([
function (next) {
posts.getPostFields(data.pid, ['pid', 'tid', 'uid', 'content', 'deleted'], next);
},
function (postData, next) {
if (parseInt(postData.deleted, 10) === 1) {
return next(new Error('[[error:post-deleted]]'));
}
post = postData;
topics.getTopicFields(post.tid, ['title', 'cid'], next);
},
function (topicData, next) {
post.topic = topicData;
async.parallel({
isAdminOrMod: function (next) {
privileges.categories.isAdminOrMod(post.topic.cid, socket.uid, next);
},
userData: function (next) {
user.getUserFields(socket.uid, ['username', 'reputation', 'banned'], next);
}
}, next);
},
function (user, next) {
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
if (!user.isAdminOrMod && parseInt(user.userData.reputation, 10) < minimumReputation) {
return next(new Error('[[error:not-enough-reputation-to-flag]]'));
}
if (parseInt(user.banned, 10) === 1) {
return next(new Error('[[error:user-banned]]'));
}
flaggingUser = user.userData;
flaggingUser.uid = socket.uid;
posts.flag(post, socket.uid, data.reason, next);
},
function (next) {
async.parallel({
post: function (next) {
posts.parsePost(post, next);
},
admins: function (next) {
groups.getMembers('administrators', 0, -1, next);
},
globalMods: function (next) {
groups.getMembers('Global Moderators', 0, -1, next);
},
moderators: function (next) {
groups.getMembers('cid:' + post.topic.cid + ':privileges:mods', 0, -1, next);
}
}, next);
},
function (results, next) {
var title = S(post.topic.title).decodeHTMLEntities().s;
var titleEscaped = title.replace(/%/g, '&#37;').replace(/,/g, '&#44;');
notifications.create({
bodyShort: '[[notifications:user_flagged_post_in, ' + flaggingUser.username + ', ' + titleEscaped + ']]',
bodyLong: post.content,
pid: data.pid,
path: '/post/' + data.pid,
nid: 'post_flag:' + data.pid + ':uid:' + socket.uid,
from: socket.uid,
mergeId: 'notifications:user_flagged_post_in|' + data.pid,
topicTitle: post.topic.title
}, function (err, notification) {
if (err || !notification) {
return next(err);
}
plugins.fireHook('action:post.flag', {post: post, reason: data.reason, flaggingUser: flaggingUser});
notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), next);
});
}
], callback);
};
SocketPosts.dismissFlag = function (socket, pid, callback) {
if (!pid || !socket.uid) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
function (next) {
user.isAdminOrGlobalMod(socket.uid, next);
},
function (isAdminOrGlobalModerator, next) {
if (!isAdminOrGlobalModerator) {
return next(new Error('[[no-privileges]]'));
}
posts.dismissFlag(pid, next);
}
], callback);
};
SocketPosts.dismissAllFlags = function (socket, data, callback) {
async.waterfall([
function (next) {
user.isAdminOrGlobalMod(socket.uid, next);
},
function (isAdminOrGlobalModerator, next) {
if (!isAdminOrGlobalModerator) {
return next(new Error('[[no-privileges]]'));
}
posts.dismissAllFlags(next);
}
], callback);
};
SocketPosts.updateFlag = function (socket, data, callback) {
if (!data || !(data.pid && data.data)) {
return callback(new Error('[[error:invalid-data]]'));
}
var payload = {};
async.waterfall([
function (next) {
async.parallel([
async.apply(user.isAdminOrGlobalMod, socket.uid),
async.apply(user.isModeratorOfAnyCategory, socket.uid)
], function (err, results) {
next(err, results[0] || results[1]);
});
},
function (allowed, next) {
if (!allowed) {
return next(new Error('[[no-privileges]]'));
}
// Translate form data into object
payload = data.data.reduce(function (memo, cur) {
memo[cur.name] = cur.value;
return memo;
}, payload);
posts.updateFlagData(socket.uid, data.pid, payload, next);
}
], callback);
};
};

View File

@@ -98,7 +98,7 @@ module.exports = function (SocketTopics) {
var start = parseInt(data.after, 10);
var stop = start + 9;
topics.getUnreadTopics(data.cid, socket.uid, start, stop, data.filter, callback);
topics.getUnreadTopics({cid: data.cid, uid: socket.uid, start: start, stop: stop, filter: data.filter}, callback);
};
SocketTopics.loadMoreRecentTopics = function (socket, data, callback) {

View File

@@ -51,7 +51,7 @@ module.exports = function (SocketTopics) {
SocketTopics.markCategoryTopicsRead = function (socket, cid, callback) {
async.waterfall([
function (next) {
topics.getUnreadTids(cid, socket.uid, '', next);
topics.getUnreadTids({cid: cid, uid: socket.uid, filter: ''}, next);
},
function (tids, next) {
SocketTopics.markAsRead(socket, tids, next);

View File

@@ -99,6 +99,9 @@ module.exports = function (SocketUser) {
uploadedpicture: '',
picture: userData.uploadedpicture === userData.picture ? '' : userData.picture // if current picture is uploaded picture, reset to user icon
}, next);
},
function (next) {
plugins.fireHook('action:user.removeUploadedPicture', {callerUid: socket.uid, uid: data.uid}, next);
}
], callback);
};

View File

@@ -162,7 +162,7 @@ module.exports = function (SocketUser) {
data.email = oldUserData.email;
}
user.updateProfile(data.uid, data, next);
user.updateProfile(socket.uid, data, next);
},
function (userData, next) {
function log(type, eventData) {

View File

@@ -2,6 +2,7 @@
'use strict';
var async = require('async');
var _ = require('underscore');
var validator = require('validator');
var S = require('string');
var db = require('../database');
@@ -82,7 +83,7 @@ module.exports = function (Topics) {
], next);
},
function (results, next) {
plugins.fireHook('action:topic.save', topicData);
plugins.fireHook('action:topic.save', {topic: _.clone(topicData)});
next(null, topicData.tid);
}
], callback);
@@ -174,7 +175,7 @@ module.exports = function (Topics) {
data.postData.index = 0;
analytics.increment(['topics', 'topics:byCid:' + data.topicData.cid]);
plugins.fireHook('action:topic.post', data.topicData);
plugins.fireHook('action:topic.post', {topic: data.topicData, post: data.postData});
if (parseInt(uid, 10)) {
user.notifications.sendTopicNotificationToFollowers(uid, data.topicData, data.postData);
@@ -269,7 +270,7 @@ module.exports = function (Topics) {
Topics.notifyFollowers(postData, uid);
analytics.increment(['posts', 'posts:byCid:' + cid]);
plugins.fireHook('action:topic.reply', postData);
plugins.fireHook('action:topic.reply', {post: _.clone(postData)});
next(null, postData);
}

View File

@@ -137,15 +137,18 @@ module.exports = function (Topics) {
function (next) {
reduceCounters(tid, next);
}
], next);
], function (err) {
next(err);
});
},
function (next) {
Topics.getTopicData(tid, next);
},
function (topicData, next) {
plugins.fireHook('action:topic.purge', {topic: topicData, uid: uid});
db.delete('topic:' + tid, next);
}
], function (err) {
if (err) {
return callback(err);
}
plugins.fireHook('action:topic.purge', tid);
db.delete('topic:' + tid, callback);
});
], callback);
};
function deleteFromFollowersIgnorers(tid, callback) {

View File

@@ -2,15 +2,13 @@
'use strict';
var async = require('async');
var winston = require('winston');
var db = require('../database');
var user = require('../user');
var posts = require('../posts');
var privileges = require('../privileges');
var plugins = require('../plugins');
var meta = require('../meta');
module.exports = function (Topics) {
Topics.createTopicFromPosts = function (uid, title, pids, fromTid, callback) {
@@ -55,11 +53,14 @@ module.exports = function (Topics) {
}
Topics.create({uid: results.postData.uid, title: title, cid: cid}, next);
},
function (results, next) {
Topics.updateTopicBookmarks(fromTid, pids, function () { next( null, results );} );
function (_tid, next) {
Topics.updateTopicBookmarks(fromTid, pids, function (err) {
next(err, _tid);
});
},
function (_tid, next) {
function move(pid, next) {
tid = _tid;
async.eachSeries(pids, function (pid, next) {
privileges.posts.canEdit(pid, uid, function (err, canEdit) {
if (err || !canEdit.flag) {
return next(err || new Error(canEdit.message));
@@ -67,14 +68,13 @@ module.exports = function (Topics) {
Topics.movePostToTopic(pid, tid, next);
});
}
tid = _tid;
async.eachSeries(pids, move, next);
}, next);
},
function (next) {
Topics.updateTimestamp(tid, Date.now(), next);
},
function (next) {
plugins.fireHook('action:topic.fork', {tid: tid, fromTid: fromTid, uid: uid});
Topics.getTopicData(tid, next);
}
], callback);

View File

@@ -56,9 +56,9 @@ module.exports = function (Topics) {
topicData.deleted = isDelete ? 1 : 0;
if (isDelete) {
plugins.fireHook('action:topic.delete', topicData);
plugins.fireHook('action:topic.delete', {topic: topicData, uid: uid});
} else {
plugins.fireHook('action:topic.restore', topicData);
plugins.fireHook('action:topic.restore', {topic: topicData, uid: uid});
}
var data = {
@@ -114,18 +114,18 @@ module.exports = function (Topics) {
function toggleLock(tid, uid, lock, callback) {
callback = callback || function () {};
var cid;
var topicData;
async.waterfall([
function (next) {
Topics.getTopicField(tid, 'cid', next);
Topics.getTopicFields(tid, ['tid', 'uid', 'cid'], next);
},
function (_cid, next) {
cid = _cid;
if (!cid) {
function (_topicData, next) {
topicData = _topicData;
if (!topicData || !topicData.cid) {
return next(new Error('[[error:no-topic]]'));
}
privileges.categories.isAdminOrMod(cid, uid, next);
privileges.categories.isAdminOrMod(topicData.cid, uid, next);
},
function (isAdminOrMod, next) {
if (!isAdminOrMod) {
@@ -135,16 +135,11 @@ module.exports = function (Topics) {
Topics.setTopicField(tid, 'locked', lock ? 1 : 0, next);
},
function (next) {
var data = {
tid: tid,
isLocked: lock,
uid: uid,
cid: cid
};
topicData.isLocked = lock;
plugins.fireHook('action:topic.lock', data);
plugins.fireHook('action:topic.lock', {topic: _.clone(topicData), uid: uid});
next(null, data);
next(null, topicData);
}
], callback);
}
@@ -167,7 +162,7 @@ module.exports = function (Topics) {
if (!exists) {
return callback(new Error('[[error:no-topic]]'));
}
Topics.getTopicFields(tid, ['cid', 'lastposttime', 'postcount'], next);
Topics.getTopicFields(tid, ['uid', 'tid', 'cid', 'lastposttime', 'postcount'], next);
},
function (_topicData, next) {
topicData = _topicData;
@@ -198,16 +193,12 @@ module.exports = function (Topics) {
], next);
},
function (results, next) {
var data = {
tid: tid,
isPinned: pin,
uid: uid,
cid: topicData.cid
};
plugins.fireHook('action:topic.pin', data);
topicData.isPinned = pin;
next(null, data);
plugins.fireHook('action:topic.pin', {topic: _.clone(topicData), uid: uid});
next(null, topicData);
}
], callback);
}
@@ -225,7 +216,7 @@ module.exports = function (Topics) {
var uniqueCids = _.unique(topicData.map(function (topicData) {
return topicData && parseInt(topicData.cid, 10);
}));
if (uniqueCids.length > 1 || !uniqueCids.length || !uniqueCids[0]) {
return next(new Error('[[error:invalid-data]]'));
}
@@ -249,7 +240,7 @@ module.exports = function (Topics) {
setImmediate(next);
}
}
], next);
], next);
}, next);
}
], callback);

View File

@@ -18,14 +18,13 @@ module.exports = function (Topics) {
callback = filter;
filter = '';
}
Topics.getUnreadTids(0, uid, filter, function (err, tids) {
Topics.getUnreadTids({cid: 0, uid: uid, filter: filter}, function (err, tids) {
callback(err, Array.isArray(tids) ? tids.length : 0);
});
};
Topics.getUnreadTopics = function (cid, uid, start, stop, filter, callback) {
Topics.getUnreadTopics = function (params, callback) {
var unreadTopics = {
showSelect: true,
nextStart : 0,
@@ -34,7 +33,7 @@ module.exports = function (Topics) {
async.waterfall([
function (next) {
Topics.getUnreadTids(cid, uid, filter, next);
Topics.getUnreadTids(params, next);
},
function (tids, next) {
unreadTopics.topicCount = tids.length;
@@ -43,13 +42,13 @@ module.exports = function (Topics) {
return next(null, []);
}
if (stop === -1) {
tids = tids.slice(start);
if (params.stop === -1) {
tids = tids.slice(params.start);
} else {
tids = tids.slice(start, stop + 1);
tids = tids.slice(params.start, params.stop + 1);
}
Topics.getTopicsByTids(tids, uid, next);
Topics.getTopicsByTids(tids, params.uid, next);
},
function (topicData, next) {
if (!Array.isArray(topicData) || !topicData.length) {
@@ -57,7 +56,7 @@ module.exports = function (Topics) {
}
unreadTopics.topics = topicData;
unreadTopics.nextStart = stop + 1;
unreadTopics.nextStart = params.stop + 1;
next(null, unreadTopics);
}
], callback);
@@ -67,21 +66,19 @@ module.exports = function (Topics) {
return Date.now() - (parseInt(meta.config.unreadCutoff, 10) || 2) * 86400000;
};
Topics.getUnreadTids = function (cid, uid, filter, callback) {
uid = parseInt(uid, 10);
Topics.getUnreadTids = function (params, callback) {
var uid = parseInt(params.uid, 10);
if (uid === 0) {
return callback(null, []);
}
var cutoff = Topics.unreadCutoff();
var cutoff = params.cutoff || Topics.unreadCutoff();
var ignoredCids;
async.waterfall([
function (next) {
async.parallel({
ignoredCids: function (next) {
if (filter === 'watched') {
if (params.filter === 'watched') {
return next(null, []);
}
user.getIgnoredCategories(uid, next);
@@ -121,7 +118,7 @@ module.exports = function (Topics) {
if (results.ignoredTids.indexOf(recentTopic.value.toString()) !== -1) {
return false;
}
switch (filter) {
switch (params.filter) {
case 'new':
return !userRead[recentTopic.value];
default:
@@ -133,7 +130,7 @@ module.exports = function (Topics) {
return array.indexOf(tid) === index;
});
if (filter === 'watched') {
if (params.filter === 'watched') {
Topics.filterWatchedTids(tids, uid, next);
} else {
next(null, tids);
@@ -143,7 +140,7 @@ module.exports = function (Topics) {
tids = tids.slice(0, 200);
filterTopics(uid, tids, cid, ignoredCids, filter, next);
filterTopics(uid, tids, params.cid, ignoredCids, params.filter, next);
}
], callback);
};

View File

@@ -13,7 +13,7 @@ var schemaDate;
var thisSchemaDate;
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema
var latestSchema = Date.UTC(2016, 10, 22);
var latestSchema = Date.UTC(2016, 11, 7);
Upgrade.check = function (callback) {
db.get('schemaDate', function (err, value) {
@@ -277,7 +277,7 @@ Upgrade.upgrade = function (callback) {
if (schemaDate < thisSchemaDate) {
updatesMade = true;
winston.info('[2016/11/25] Creating sorted sets for pinned topcis');
winston.info('[2016/11/25] Creating sorted sets for pinned topics');
var topics = require('./topics');
var batch = require('./batch');
@@ -314,6 +314,101 @@ Upgrade.upgrade = function (callback) {
next();
}
},
function (next) {
thisSchemaDate = Date.UTC(2016, 11, 7);
if (schemaDate < thisSchemaDate) {
updatesMade = true;
winston.info('[2016/12/07] Migrating flags to new schema (#5232)');
var batch = require('./batch');
var posts = require('./posts');
var flags = require('./flags');
var migrated = 0;
batch.processSortedSet('posts:pid', function (ids, next) {
posts.getPostsByPids(ids, 1, function (err, posts) {
if (err) {
return next(err);
}
posts = posts.filter(function (post) {
return post.hasOwnProperty('flags');
});
async.each(posts, function (post, next) {
async.parallel({
uids: async.apply(db.getSortedSetRangeWithScores, 'pid:' + post.pid + ':flag:uids', 0, -1),
reasons: async.apply(db.getSortedSetRange, 'pid:' + post.pid + ':flag:uid:reason', 0, -1)
}, function (err, data) {
if (err) {
return next(err);
}
// Adding in another check here in case a post was improperly dismissed (flag count > 1 but no flags in db)
if (!data.uids.length || !data.reasons.length) {
return setImmediate(next);
}
// Just take the first entry
var datetime = data.uids[0].score;
var reason = data.reasons[0].split(':')[1];
var flagObj;
async.waterfall([
async.apply(flags.create, 'post', post.pid, data.uids[0].value, reason, datetime),
function (_flagObj, next) {
flagObj = _flagObj;
if (post['flag:state'] || post['flag:assignee']) {
flags.update(flagObj.flagId, 1, {
state: post['flag:state'],
assignee: post['flag:assignee'],
datetime: datetime
}, next);
} else {
setImmediate(next);
}
},
function (next) {
if (post.hasOwnProperty('flag:notes') && post['flag:notes'].length) {
try {
var history = JSON.parse(post['flag:history']);
history = history.filter(function (event) {
return event.type === 'notes';
})[0];
flags.appendNote(flagObj.flagId, history.uid, post['flag:notes'], history.timestamp, next);
} catch (e) {
next(e);
}
} else {
setImmediate(next);
}
}
], function (err) {
if (err && err.message === '[[error:already-flagged]]') {
// Already flagged, no need to parse, but not an error
next();
} else {
next(err);
}
});
});
}, next);
});
}, function (err) {
if (err) {
return next(err);
}
winston.info('[2016/12/07] Migrating flags to new schema (#5232) - done');
Upgrade.update(thisSchemaDate, next);
});
} else {
winston.info('[2016/12/07] Migrating flags to new schema (#5232) - skipped!');
next();
}
}
// Add new schema updates here
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!!
], function (err) {

View File

@@ -256,6 +256,16 @@ var meta = require('./meta');
privileges.users.isGlobalModerator(uid, callback);
};
User.isPrivileged = function (uid, callback) {
async.parallel([
async.apply(User.isAdministrator, uid),
async.apply(User.isGlobalModerator, uid),
async.apply(User.isModeratorOfAnyCategory, uid)
], function (err, results) {
callback(err, results ? results.some(Boolean) : false);
});
};
User.isAdminOrGlobalMod = function (uid, callback) {
async.parallel({
isAdmin: async.apply(User.isAdministrator, uid),

View File

@@ -6,6 +6,7 @@ var db = require('../database');
var posts = require('../posts');
var plugins = require('../plugins');
var winston = require('winston');
var flags = require('../flags');
module.exports = function (User) {
@@ -55,14 +56,4 @@ module.exports = function (User) {
}
], callback);
};
User.resetFlags = function (uids, callback) {
if (!Array.isArray(uids) || !uids.length) {
return callback();
}
async.eachSeries(uids, function (uid, next) {
posts.dismissUserFlags(uid, next);
}, callback);
};
};

View File

@@ -141,7 +141,7 @@ module.exports = function (User) {
if (userNameChanged) {
User.notifications.sendNameChangeNotification(userData.uid, userData.username);
}
plugins.fireHook('action:user.create', userData);
plugins.fireHook('action:user.create', {user: userData});
next(null, userData.uid);
}
], callback);

View File

@@ -34,11 +34,20 @@ module.exports = function (User) {
}
}
if (!Array.isArray(uids) || !uids.length) {
// Eliminate duplicates and build ref table
var uniqueUids = uids.filter(function (uid, index) {
return index === uids.indexOf(uid);
});
var ref = uniqueUids.reduce(function (memo, cur, idx) {
memo[cur] = idx;
return memo;
}, {});
if (!Array.isArray(uniqueUids) || !uniqueUids.length) {
return callback(null, []);
}
var keys = uids.map(function (uid) {
var keys = uniqueUids.map(function (uid) {
return 'user:' + uid;
});
@@ -60,6 +69,10 @@ module.exports = function (User) {
return callback(err);
}
users = uids.map(function (uid) {
return users[ref[uid]];
});
modifyUserData(users, fieldsToRemove, callback);
});
};
@@ -80,7 +93,16 @@ module.exports = function (User) {
return callback(null, []);
}
var keys = uids.map(function (uid) {
// Eliminate duplicates and build ref table
var uniqueUids = uids.filter(function (uid, index) {
return index === uids.indexOf(uid);
});
var ref = uniqueUids.reduce(function (memo, cur, idx) {
memo[cur] = idx;
return memo;
}, {});
var keys = uniqueUids.map(function (uid) {
return 'user:' + uid;
});
@@ -89,6 +111,10 @@ module.exports = function (User) {
return callback(err);
}
users = uids.map(function (uid) {
return users[ref[uid]];
});
modifyUserData(users, [], callback);
});
};

View File

@@ -57,7 +57,8 @@ module.exports = function (User) {
path: picture.path,
extension: extension,
width: imageDimension,
height: imageDimension
height: imageDimension,
write: false,
}, next);
},
function (next) {

View File

@@ -16,6 +16,9 @@ module.exports = function (User) {
var fields = ['username', 'email', 'fullname', 'website', 'location',
'groupTitle', 'birthday', 'signature', 'aboutme'];
var updateUid = data.uid;
var oldData;
async.waterfall([
function (next) {
plugins.fireHook('filter:user.updateProfile', {uid: uid, data: data, fields: fields}, next);
@@ -27,14 +30,18 @@ module.exports = function (User) {
async.series([
async.apply(isAboutMeValid, data),
async.apply(isSignatureValid, data),
async.apply(isEmailAvailable, data, uid),
async.apply(isUsernameAvailable, data, uid),
async.apply(isEmailAvailable, data, updateUid),
async.apply(isUsernameAvailable, data, updateUid),
async.apply(isGroupTitleValid, data)
], function (err) {
next(err);
});
},
function (next) {
User.getUserFields(updateUid, fields, next);
},
function (_oldData, next) {
oldData = _oldData;
async.each(fields, function (field, next) {
if (!(data[field] !== undefined && typeof data[field] === 'string')) {
return next();
@@ -43,21 +50,21 @@ module.exports = function (User) {
data[field] = data[field].trim();
if (field === 'email') {
return updateEmail(uid, data.email, next);
return updateEmail(updateUid, data.email, next);
} else if (field === 'username') {
return updateUsername(uid, data.username, next);
return updateUsername(updateUid, data.username, next);
} else if (field === 'fullname') {
return updateFullname(uid, data.fullname, next);
return updateFullname(updateUid, data.fullname, next);
} else if (field === 'signature') {
data[field] = S(data[field]).stripTags().s;
}
User.setUserField(uid, field, data[field], next);
User.setUserField(updateUid, field, data[field], next);
}, next);
},
function (next) {
plugins.fireHook('action:user.updateProfile', {data: data, uid: uid});
User.getUserFields(uid, ['email', 'username', 'userslug', 'picture', 'icon:text', 'icon:bgColor'], next);
plugins.fireHook('action:user.updateProfile', {uid: uid, data: data, fields: fields, oldData: oldData});
User.getUserFields(updateUid, ['email', 'username', 'userslug', 'picture', 'icon:text', 'icon:bgColor'], next);
}
], callback);
};

View File

@@ -4,8 +4,8 @@
<title>{title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="{relative_path}/admin.css?{cache-buster}" />
<link rel="stylesheet" type="text/css" href="{relative_path}/vendor/mdl/mdl.min.css?{cache-buster}" />
<link rel="stylesheet" type="text/css" href="{relative_path}/assets/admin.css?{cache-buster}" />
<link rel="stylesheet" type="text/css" href="{relative_path}/assets/vendor/mdl/mdl.min.css?{cache-buster}" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<script>
@@ -21,17 +21,17 @@
</script>
<script src="https://storage.googleapis.com/code.getmdl.io/1.0.3/material.min.js"></script>
<script type="text/javascript" src="{relative_path}/vendor/jquery/sortable/Sortable.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/acp.min.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/vendor/colorpicker/colorpicker.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/src/admin/admin.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/vendor/ace/ace.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/vendor/semver/semver.browser.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/vendor/jquery/serializeObject/jquery.ba-serializeobject.min.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/vendor/jquery/deserialize/jquery.deserialize.min.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/vendor/snackbar/snackbar.min.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/vendor/slideout/slideout.min.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/vendor/nprogress.min.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/assets/vendor/jquery/sortable/Sortable.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/assets/acp.min.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/assets/vendor/colorpicker/colorpicker.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/assets/src/admin/admin.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/assets/vendor/ace/ace.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/assets/vendor/semver/semver.browser.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/assets/vendor/jquery/serializeObject/jquery.ba-serializeobject.min.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/assets/vendor/jquery/deserialize/jquery.deserialize.min.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/assets/vendor/snackbar/snackbar.min.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/assets/vendor/slideout/slideout.min.js?{cache-buster}"></script>
<script type="text/javascript" src="{relative_path}/assets/vendor/nprogress.min.js?{cache-buster}"></script>
<!-- BEGIN scripts -->
<script type="text/javascript" src="{scripts.src}"></script>

View File

@@ -1,220 +0,0 @@
<div class="flags">
<div class="col-lg-12">
<div class="text-center">
<div class="panel panel-default">
<div class="panel-body">
<div><canvas id="flags:daily" height="250"></canvas></div>
<p>
</p>
</div>
<div class="panel-footer"><small>[[admin/manage/flags:daily]]</small></div>
</div>
</div>
<form id="flag-search" method="GET" action="flags">
<div class="form-group">
<div>
<div>
<label>[[admin/manage/flags:by-user]]</label>
<input type="text" class="form-control" id="byUsername" placeholder="[[admin/manage/flags:by-user-search]]" name="byUsername" value="{byUsername}">
</div>
</div>
</div>
<div class="form-group">
<div>
<div>
<label>[[admin/manage/flags:category]]</label>
<select class="form-control" id="category-selector" name="cid">
<option value="">[[unread:all_categories]]</option>
<!-- BEGIN categories -->
<option value="{categories.cid}" <!-- IF categories.selected -->selected<!-- ENDIF categories.selected -->>{categories.text}</option>
<!-- END categories -->
</select>
</div>
</div>
</div>
<div class="form-group">
<label>[[admin/manage/flags:sort-by]]</label>
<div>
<div>
<select id="flag-sort-by" class="form-control" name="sortBy">
<option value="count" <!-- IF sortByCount -->selected<!-- ENDIF sortByCount -->>[[admin/manage/flags:sort-by.most-flags]]</option>
<option value="time" <!-- IF sortByTime -->selected<!-- ENDIF sortByTime -->>[[admin/manage/flags:sort-by.most-recent]]</option>
</select>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">[[admin/manage/flags:search]]</button>
<button class="btn btn-primary" id="dismissAll">[[admin/manage/flags:dismiss-all]]</button>
</form>
<hr/>
<div data-next="{next}">
<div component="posts/flags" class="panel-group post-container" id="accordion" role="tablist" aria-multiselectable="true" data-next="{next}">
<!-- IF !posts.length -->
<div class="alert alert-success">
[[admin/manage/flags:none-flagged]]
</div>
<!-- ENDIF !posts.length -->
<!-- BEGIN posts -->
<div class="panel panel-default" component="posts/flag" data-pid="{../pid}">
<div class="panel-heading" role="tab">
<h4 class="panel-title">
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#flag-pid-{posts.pid}" aria-expanded="true" aria-controls="flag-pid-{posts.pid}">
<!-- IF ../flagData.assignee -->
<div class="pull-right">
<!-- IF ../flagData.assigneeUser.picture -->
<img class="avatar avatar-xs" title="{../flagData.assigneeUser.username}" src="{../flagData.assigneeUser.picture}">
<!-- ELSE -->
<div class="avatar avatar-xs" title="{../flagData.assigneeUser.username}" style="background-color: {../flagData.assigneeUser.icon:bgColor};">{../flagData.assigneeUser.icon:text}</div>
<!-- ENDIF ../flagData.assigneeUser.picture -->
</div>
<!-- ENDIF ../flagData.assignee -->
<span class="label <!-- IF ../flagData.labelClass -->label-{../flagData.labelClass}<!-- ELSE -->label-info<!-- ENDIF ../flagData.labelClass -->">[[topic:flag_manage_state_<!-- IF ../flagData.state -->{../flagData.state}<!-- ELSE -->open<!-- ENDIF ../flagData.state -->]]</span>
&nbsp;[[topic:flag_manage_title, {posts.category.name}]]
<small><span class="timeago" title="{posts.timestampISO}"></span></small>
</a>
</h4>
</div>
<div id="flag-pid-{posts.pid}" class="panel-collapse collapse" role="tabpanel">
<div class="panel-body">
<div class="row" data-pid="{posts.pid}" data-tid="{posts.topic.tid}">
<div class="col-sm-8">
<div class="well flag-post-body">
<a href="{config.relative_path}/user/{../user.userslug}">
<!-- IF ../user.picture -->
<img title="{posts.user.username}" src="{../user.picture}">
<!-- ELSE -->
<div title="{posts.user.username}" class="user-icon" style="background-color: {../user.icon:bgColor};">{../user.icon:text}</div>
<!-- ENDIF ../user.picture -->
</a>
<a href="{config.relative_path}/user/{../user.userslug}">
<strong><span>{../user.username}</span></strong>
</a>
<div class="content">
<p>{posts.content}</p>
</div>
<small>
<span class="pull-right">
[[posted-in, <a href="{config.relative_path}/category/{posts.category.slug}" target="_blank"><i class="fa {posts.category.icon}"></i> {posts.category.name}</a>]],
<span class="timeago" title="{posts.timestampISO}"></span> &bull;
<a href="{config.relative_path}/post/{posts.pid}" target="_blank">[[admin/manage/flags:read-more]]</a>
</span>
</small>
</div>
</div>
<div class="col-sm-4">
<i class="fa fa-flag"></i>
[[admin/manage/flags:flagged-x-times, {posts.flags}]]
<blockquote class="flag-reporters">
<ul>
<!-- BEGIN posts.flagReasons -->
<li>
<a target="_blank" href="{config.relative_path}/user/{../user.userslug}">
<!-- IF ../user.picture -->
<img src="{../user.picture}" />
<!-- ELSE -->
<div class="user-icon" style="background-color: {../user.icon:bgColor};">{../user.icon:text}</div>
<!-- ENDIF ../user.picture -->
{../user.username}
</a>: "{posts.flagReasons.reason}"
</li>
<!-- END posts.flagReasons -->
</ul>
</blockquote>
<div class="btn-group">
<button class="btn btn-sm btn-success dismiss">
[[admin/manage/flags:dismiss]]
</button>
<button class="btn btn-sm btn-danger delete">
[[admin/manage/flags:delete-post]]
</button>
</div>
</div>
</div>
<hr />
<div class="row">
<div class="col-sm-6">
<form role="form">
<div class="form-group">
<label for="{posts.pid}-assignee">
[[topic:flag_manage_assignee]]
</label>
<select class="form-control" id="{posts.pid}-assignee" name="assignee">
<!-- BEGIN assignees -->
<option value="{assignees.uid}">{assignees.username}</option>
<!-- END assignees -->
</select>
</div>
<div class="form-group">
<label for="{posts.pid}-state">
[[topic:flag_manage_state]]
</label>
<select class="form-control" id="{posts.pid}-state" name="state">
<option value="open">
[[topic:flag_manage_state_open]]
</option>
<option value="wip">
[[topic:flag_manage_state_wip]]
</option>
<option value="resolved">
[[topic:flag_manage_state_resolved]]
</option>
<option value="rejected">
[[topic:flag_manage_state_rejected]]
</option>
</select>
</div>
<div class="form-group">
<label for="{posts.pid}-notes">
[[topic:flag_manage_notes]]
</label>
<textarea class="form-control" id="{posts.pid}-notes" name="notes"></textarea>
</div>
<button type="button" component="posts/flag/update" class="btn btn-sm btn-primary btn-block">
[[topic:flag_manage_update]]
</button>
</form>
</div>
<div class="col-sm-6">
<h5>[[topic:flag_manage_history]]</h5>
<!-- IF !posts.flagData.history.length -->
<div class="alert alert-info">
[[topic:flag_manage_no_history]]
</div>
<!-- ELSE -->
<ul class="list-group" component="posts/flag/history">
<!-- BEGIN posts.flagData.history -->
<li class="list-group-item">
<div class="pull-right"><small><span class="timeago" title="{posts.flagData.history.timestampISO}"></span></small></div>
<!-- IF ../user.picture -->
<img class="avatar avatar-sm avatar-rounded" src="{../user.picture}" title="{../user.username}" />
<!-- ELSE -->
<div class="avatar avatar-sm avatar-rounded" style="background-color: {../user.icon:bgColor};" title="{../user.username}">{../user.icon:text}</div>
<!-- ENDIF ../user.picture -->
[[topic:flag_manage_history_{posts.flagData.history.type}, {posts.flagData.history.label}]]
</li>
<!-- END posts.flagData.history -->
</ul>
<!-- ENDIF !posts.flagData.history.length -->
</div>
</div>
</div>
</div>
</div>
<!-- END posts -->
<!-- IMPORT partials/paginator.tpl -->
</div>
</div>
</div>
</div>

View File

@@ -19,7 +19,6 @@
<li><a href="#" class="ban-user-temporary"><i class="fa fa-fw fa-clock-o"></i>[[admin/manage/users:temp-ban]]</a></li>
<li><a href="#" class="unban-user"><i class="fa fa-fw fa-comment-o"></i> [[admin/manage/users:unban]]</a></li>
<li><a href="#" class="reset-lockout"><i class="fa fa-fw fa-unlock"></i> [[admin/manage/users:reset-lockout]]</a></li>
<li><a href="#" class="reset-flags"><i class="fa fa-fw fa-flag"></i> [[admin/manage/users:reset-flags]]</a></li>
<li class="divider"></li>
<li><a href="#" class="delete-user"><i class="fa fa-fw fa-trash-o"></i> [[admin/manage/users:delete]]</a></li>
<li><a href="#" class="delete-user-and-content"><i class="fa fa-fw fa-trash-o"></i> [[admin/manage/users:purge]]</a></li>

View File

@@ -19,7 +19,6 @@
<li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li>
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>
<li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li>
<li><a href="{relative_path}/admin/manage/flags">[[admin/menu:manage/flags]]</a></li>
<li><a href="{relative_path}/admin/manage/ip-blacklist">[[admin/menu:manage/ip-blacklist]]</a></li>
</ul>
</section>
@@ -192,7 +191,6 @@
<li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li>
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>
<li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li>
<li><a href="{relative_path}/admin/manage/flags">[[admin/menu:manage/flags]]</a></li>
<li><a href="{relative_path}/admin/manage/ip-blacklist">[[admin/menu:manage/ip-blacklist]]</a></li>
</ul>
</li>

View File

@@ -113,6 +113,23 @@
</div>
</div>
<div class="row">
<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/general:search-default-sort-by]]</div>
<div class="col-sm-10 col-xs-12">
<select id="post-sort-by" class="form-control" data-field="searchDefaultSortBy">
<option value="relevance">[[search:relevance]]</option>
<option value="timestamp">[[search:post-time]]</option>
<option value="teaser.timestamp">[[search:last-reply-time]]</option>
<option value="topic.title">[[search:topic-title]]</option>
<option value="topic.postcount">[[search:number-of-replies]]</option>
<option value="topic.viewcount">[[search:number-of-views]]</option>
<option value="topic.timestamp">[[search:topic-start-date]]</option>
<option value="user.username">[[search:username]]</option>
<option value="category.name">[[search:category]]</option>
</select>
</div>
</div>
<div class="row">
<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/general:outgoing-links]]</div>
<div class="col-sm-10 col-xs-12">

View File

@@ -43,7 +43,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" value="{config.relative_path}/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" value="{config.relative_path}/assets/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" /><br />
</form>
</div>
</div>

View File

@@ -131,7 +131,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" value="{config.relative_path}/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" value="{config.relative_path}/assets/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" />
</form>
</div>
</div>

View File

@@ -45,7 +45,7 @@ server.on('error', function (err) {
winston.error(err);
if (err.code === 'EADDRINUSE') {
winston.error('NodeBB address in use, exiting...');
process.exit(0);
process.exit(1);
} else {
throw err;
}
@@ -88,7 +88,6 @@ function initializeNodeBB(callback) {
plugins.init(app, middleware, next);
},
async.apply(plugins.fireHook, 'static:assets.prepare', {}),
async.apply(meta.js.bridgeModules, app),
function (next) {
plugins.fireHook('static:app.preload', {
app: app,
@@ -104,9 +103,6 @@ function initializeNodeBB(callback) {
},
function (next) {
async.series([
async.apply(meta.js.getFromFile, 'nodebb.min.js'),
async.apply(meta.js.getFromFile, 'acp.min.js'),
async.apply(meta.css.getFromFile),
async.apply(meta.sounds.init),
async.apply(languages.init),
async.apply(meta.blacklist.load)
@@ -266,11 +262,11 @@ module.exports.testSocket = function (socketPath, callback) {
var file = require('./file');
async.series([
function (next) {
file.exists(socketPath, function (exists) {
file.exists(socketPath, function (err, exists) {
if (exists) {
next();
} else {
callback();
callback(err);
}
});
},

View File

@@ -3,6 +3,7 @@
var fs = require('fs');
var path = require('path');
var async = require('async');
var nconf = require('nconf');
var plugins = require('../plugins');
var admin = {};
@@ -25,7 +26,7 @@ admin.get = function (callback) {
plugins.fireHook('filter:widgets.getWidgets', [], next);
},
adminTemplate: function (next) {
fs.readFile(path.resolve(__dirname, '../../public/templates/admin/partials/widget-settings.tpl'), 'utf8', next);
fs.readFile(path.resolve(nconf.get('views_dir'), 'admin/partials/widget-settings.tpl'), 'utf8', next);
}
}, function (err, widgetData) {
if (err) {